Index: examples/MultipartFileUploadApp.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/examples/MultipartFileUploadApp.java,v retrieving revision 1.9 diff -u -r1.9 MultipartFileUploadApp.java --- examples/MultipartFileUploadApp.java 22 Feb 2004 18:08:45 -0000 1.9 +++ examples/MultipartFileUploadApp.java 23 Sep 2004 02:45:38 -0000 @@ -39,8 +39,8 @@ import javax.swing.DefaultComboBoxModel; import javax.swing.JButton; -import javax.swing.JComboBox; import javax.swing.JCheckBox; +import javax.swing.JComboBox; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; @@ -50,7 +50,10 @@ import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.httpclient.methods.MultipartPostMethod; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.multipart.FilePart; +import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity; +import org.apache.commons.httpclient.methods.multipart.Part; import org.apache.commons.httpclient.params.HttpMethodParams; /** @@ -147,14 +150,18 @@ cmbURLModel.addElement(targetURL); } - MultipartPostMethod filePost = - new MultipartPostMethod(targetURL); + PostMethod filePost = new PostMethod(targetURL); filePost.getParams().setBooleanParameter(HttpMethodParams.USE_EXPECT_CONTINUE, cbxExpectHeader.isSelected()); try { appendMessage("Uploading " + targetFile.getName() + " to " + targetURL); - filePost.addParameter(targetFile.getName(), targetFile); + Part[] parts = { + new FilePart(targetFile.getName(), targetFile) + }; + filePost.setRequestEntity( + new MultipartRequestEntity(parts, filePost.getParams()) + ); HttpClient client = new HttpClient(); client.getHttpConnectionManager(). getParams().setConnectionTimeout(5000); Index: java/org/apache/commons/httpclient/methods/MultipartPostMethod.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/MultipartPostMethod.java,v retrieving revision 1.26 diff -u -r1.26 MultipartPostMethod.java --- java/org/apache/commons/httpclient/methods/MultipartPostMethod.java 13 Jun 2004 20:22:19 -0000 1.26 +++ java/org/apache/commons/httpclient/methods/MultipartPostMethod.java 23 Sep 2004 02:45:39 -0000 @@ -71,6 +71,9 @@ * @author Oleg Kalnichevski * * @since 2.0 + * + * @deprecated Use {@link org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity} + * in conjunction with {@link org.apache.commons.httpclient.methods.PostMethod} instead. */ public class MultipartPostMethod extends ExpectContinueMethod { Index: java/org/apache/commons/httpclient/methods/multipart/Part.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/multipart/Part.java,v retrieving revision 1.14 diff -u -r1.14 Part.java --- java/org/apache/commons/httpclient/methods/multipart/Part.java 18 Apr 2004 23:51:37 -0000 1.14 +++ java/org/apache/commons/httpclient/methods/multipart/Part.java 23 Sep 2004 02:45:40 -0000 @@ -55,7 +55,10 @@ //TODO: Make this configurable - /** The boundary */ + /** + * The boundary + * @deprecated use {@link org.apache.commons.httpclient.params.HttpMethodParams#MULTIPART_BOUNDARY} + */ protected static final String BOUNDARY = "----------------314159265358979323846"; /** The boundary as a byte array */ @@ -112,6 +115,7 @@ /** * Return the boundary string. * @return the boundary string + * @deprecated use {@link org.apache.commons.httpclient.params.HttpMethodParams#MULTIPART_BOUNDARY} */ public static String getBoundary() { return BOUNDARY; @@ -142,6 +146,16 @@ */ public abstract String getTransferEncoding(); + /** + * Tests if this part can be sent more than once. + * @return true if {@link #sendData(OutputStream)} can be successfully called + * more than once. + * @since 3.0 + */ + public boolean isRepeatable() { + return true; + } + /** * Write the start to the specified output stream * @param out The output stream Index: java/org/apache/commons/httpclient/params/DefaultHttpParamsFactory.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/params/DefaultHttpParamsFactory.java,v retrieving revision 1.13 diff -u -r1.13 DefaultHttpParamsFactory.java --- java/org/apache/commons/httpclient/params/DefaultHttpParamsFactory.java 19 Sep 2004 19:37:07 -0000 1.13 +++ java/org/apache/commons/httpclient/params/DefaultHttpParamsFactory.java 23 Sep 2004 02:45:40 -0000 @@ -75,6 +75,7 @@ params.setHttpElementCharset("US-ASCII"); params.setContentCharset("ISO-8859-1"); params.setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler()); + params.setParameter(HttpMethodParams.MULTIPART_BOUNDARY, "----------------314159265358979323846"); ArrayList datePatterns = new ArrayList(); datePatterns.addAll( Index: java/org/apache/commons/httpclient/params/HttpMethodParams.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/params/HttpMethodParams.java,v retrieving revision 1.15 diff -u -r1.15 HttpMethodParams.java --- java/org/apache/commons/httpclient/params/HttpMethodParams.java 17 Sep 2004 08:00:51 -0000 1.15 +++ java/org/apache/commons/httpclient/params/HttpMethodParams.java 23 Sep 2004 02:45:42 -0000 @@ -258,6 +258,14 @@ public static final String BUFFER_WARN_TRIGGER_LIMIT = "http.method.response.buffer.warnlimit"; /** + * Sets the value to use as the multipart boundary. + *

+ * This parameter expects a value if type {@link String}. + *

+ */ + public static final String MULTIPART_BOUNDARY = "http.method.multipart.boundary"; + + /** * Creates a new collection of parameters with the collection returned * by {@link #getDefaultParams()} as a parent. The collection will defer * to its parent for a default value if a particular parameter is not Index: test/org/apache/commons/httpclient/TestEffectiveHttpVersion.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestEffectiveHttpVersion.java,v retrieving revision 1.2 diff -u -r1.2 TestEffectiveHttpVersion.java --- test/org/apache/commons/httpclient/TestEffectiveHttpVersion.java 14 Sep 2004 15:50:40 -0000 1.2 +++ test/org/apache/commons/httpclient/TestEffectiveHttpVersion.java 23 Sep 2004 02:45:42 -0000 @@ -36,9 +36,6 @@ import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.params.HttpMethodParams; -import org.apache.commons.httpclient.server.HttpService; -import org.apache.commons.httpclient.server.SimpleRequest; -import org.apache.commons.httpclient.server.SimpleResponse; /** * HTTP protocol versioning tests. @@ -64,22 +61,6 @@ public static Test suite() { return new TestSuite(TestEffectiveHttpVersion.class); - } - - private class EchoService implements HttpService { - - public EchoService() { - super(); - } - - public boolean process(final SimpleRequest request, final SimpleResponse response) - throws IOException - { - HttpVersion httpversion = request.getRequestLine().getHttpVersion(); - response.setStatusLine(httpversion, HttpStatus.SC_OK); - response.setBodyString(request.getBodyString()); - return true; - } } public void testClientLevelHttpVersion() throws IOException { Index: test/org/apache/commons/httpclient/TestNoHost.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestNoHost.java,v retrieving revision 1.39 diff -u -r1.39 TestNoHost.java --- test/org/apache/commons/httpclient/TestNoHost.java 15 Sep 2004 20:42:17 -0000 1.39 +++ test/org/apache/commons/httpclient/TestNoHost.java 23 Sep 2004 02:45:42 -0000 @@ -92,6 +92,7 @@ suite.addTestSuite(TestIdleConnectionTimeout.class); suite.addTest(TestMethodAbort.suite()); suite.addTest(TestHttpParams.suite()); + suite.addTest(TestMultipartPost.suite()); return suite; } Index: test/org/apache/commons/httpclient/TestWebapp.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestWebapp.java,v retrieving revision 1.10 diff -u -r1.10 TestWebapp.java --- test/org/apache/commons/httpclient/TestWebapp.java 25 Apr 2004 12:25:09 -0000 1.10 +++ test/org/apache/commons/httpclient/TestWebapp.java 23 Sep 2004 02:45:43 -0000 @@ -67,7 +67,6 @@ suite.addTest(TestWebappRedirect.suite()); suite.addTest(TestWebappBasicAuth.suite()); suite.addTest(TestWebappPostMethod.suite()); - suite.addTest(TestWebappMultiPostMethod.suite()); suite.addTest(TestWebappNoncompliant.suite()); suite.addTest(TestProxy.suite()); return suite; Index: test/org/apache/commons/httpclient/TestWebappMultiPostMethod.java =================================================================== RCS file: test/org/apache/commons/httpclient/TestWebappMultiPostMethod.java diff -N test/org/apache/commons/httpclient/TestWebappMultiPostMethod.java --- test/org/apache/commons/httpclient/TestWebappMultiPostMethod.java 22 Feb 2004 18:08:50 -0000 1.5 +++ /dev/null 1 Jan 1970 00:00:00 -0000 @@ -1,115 +0,0 @@ -/* - * $Header: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestWebappMultiPostMethod.java,v 1.5 2004/02/22 18:08:50 olegk Exp $ - * $Revision: 1.5 $ - * $Date: 2004/02/22 18:08:50 $ - * - * ==================================================================== - * - * Copyright 2003-2004 The Apache Software Foundation - * - * 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - * [Additional notices, if required by prior licensing conditions] - * - */ - -package org.apache.commons.httpclient; - -import junit.framework.Test; -import junit.framework.TestSuite; - -import org.apache.commons.httpclient.methods.MultipartPostMethod; -import org.apache.commons.httpclient.methods.multipart.ByteArrayPartSource; -import org.apache.commons.httpclient.methods.multipart.FilePart; -import org.apache.commons.httpclient.methods.multipart.StringPart; - -/** - * Webapp tests specific to the MultiPostMethod. - * - * @author Oleg Kalnichevski - */ -public class TestWebappMultiPostMethod extends TestWebappBase { - - HttpClient httpClient; - final String paramsPath = "/" + getWebappContext() + "/params"; - final String bodyPath = "/" + getWebappContext() + "/body"; - - public TestWebappMultiPostMethod(String testName) { - super(testName); - } - - public static Test suite() { - TestSuite suite = new TestSuite(TestWebappMultiPostMethod.class); - return suite; - } - - public static void main(String args[]) { - String[] testCaseName = { TestWebappMultiPostMethod.class.getName() }; - junit.textui.TestRunner.main(testCaseName); - } - - public void setUp() { - httpClient = createHttpClient(); - } - - // ------------------------------------------------------------------ Tests - - /** - * Test that the body consisting of a string part can be posted. - */ - - public void testPostStringPart() throws Exception { - MultipartPostMethod method = new MultipartPostMethod(bodyPath); - method.addPart(new StringPart("param", "Hello", "ISO-8859-1")); - - httpClient.executeMethod(method); - - assertEquals(200,method.getStatusCode()); - String body = method.getResponseBodyAsString(); - assertTrue(body.indexOf("Content-Disposition: form-data; name=\"param\"") >= 0); - assertTrue(body.indexOf("Content-Type: text/plain; charset=ISO-8859-1") >= 0); - assertTrue(body.indexOf("Content-Transfer-Encoding: 8bit") >= 0); - assertTrue(body.indexOf("Hello") >= 0); - } - - - /** - * Test that the body consisting of a file part can be posted. - */ - public void testPostFilePart() throws Exception { - MultipartPostMethod method = new MultipartPostMethod(bodyPath); - byte[] content = "Hello".getBytes(); - method.addPart( - new FilePart( - "param1", - new ByteArrayPartSource("filename.txt", content), - "text/plain", - "ISO-8859-1")); - - httpClient.executeMethod(method); - - assertEquals(200,method.getStatusCode()); - String body = method.getResponseBodyAsString(); - assertTrue(body.indexOf("Content-Disposition: form-data; name=\"param1\"; filename=\"filename.txt\"") >= 0); - assertTrue(body.indexOf("Content-Type: text/plain; charset=ISO-8859-1") >= 0); - assertTrue(body.indexOf("Content-Transfer-Encoding: binary") >= 0); - assertTrue(body.indexOf("Hello") >= 0); - } -} - Index: test/org/apache/commons/httpclient/server/SimpleHttpServerConnection.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/server/SimpleHttpServerConnection.java,v retrieving revision 1.10 diff -u -r1.10 SimpleHttpServerConnection.java --- test/org/apache/commons/httpclient/server/SimpleHttpServerConnection.java 14 Sep 2004 15:50:41 -0000 1.10 +++ test/org/apache/commons/httpclient/server/SimpleHttpServerConnection.java 23 Sep 2004 02:45:43 -0000 @@ -31,15 +31,21 @@ package org.apache.commons.httpclient.server; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.Socket; +import org.apache.commons.httpclient.ContentLengthInputStream; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HeaderGroup; +import org.apache.commons.httpclient.HttpConstants; import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.HttpParser; import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.SimpleChunkedInputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -152,10 +158,9 @@ SimpleRequest request = null; try { - request = new SimpleRequest( - RequestLine.parseLine(line), - HttpParser.parseHeaders(in, HTTP_ELEMENT_CHARSET), - null); + RequestLine statusLine = RequestLine.parseLine(line); + Header[] headers = HttpParser.parseHeaders(in, HTTP_ELEMENT_CHARSET); + request = new SimpleRequest(statusLine, headers, getBody(statusLine, headers, in)); } catch (HttpException e) { connectionClose(); SimpleResponse response = ErrorResponse.getInstance(). @@ -169,6 +174,49 @@ } server.processRequest(this, request); out.flush(); + } + + /** + * Reads the request body into a byte[]. + * @param statusLine The request status line. + * @param headers The request headers. + * @param is The request input stream. + * @return The content as an arra of bytes. + * @throws IOException + */ + private byte[] getBody(RequestLine statusLine, Header[] headers, InputStream is) throws IOException { + + // only PUT and POST have content + if ( + !statusLine.getMethod().equalsIgnoreCase("POST") + && !statusLine.getMethod().equalsIgnoreCase("PUT") + ) { + return new byte[0]; + } + + HeaderGroup headerGroup = new HeaderGroup(); + headerGroup.setHeaders(headers); + Header contentLength = headerGroup.getFirstHeader("Content-Length"); + InputStream contentStream = is; + int length = -1; + if (contentLength != null) { + length = Integer.parseInt(contentLength.getValue().trim()); + contentStream = new ContentLengthInputStream(is, (long) length); + } else { + Header transferEncoding = headerGroup.getFirstHeader("Transfer-Encoding"); + if (transferEncoding != null && transferEncoding.getValue().indexOf("chunked") != -1) { + contentStream = new SimpleChunkedInputStream(is, HttpConstants.DEFAULT_CONTENT_CHARSET); + } + } + + byte[] buff = new byte[4096]; + int bytesRead = 0; + ByteArrayOutputStream os = new ByteArrayOutputStream(Math.max(length, 4096)); + while ((bytesRead = contentStream.read(buff)) != -1) { + os.write(buff, 0, bytesRead); + } + + return os.toByteArray(); } public InputStream getInputStream() { Index: test/org/apache/commons/httpclient/server/SimpleRequest.java =================================================================== RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/server/SimpleRequest.java,v retrieving revision 1.1 diff -u -r1.1 SimpleRequest.java --- test/org/apache/commons/httpclient/server/SimpleRequest.java 27 Feb 2004 19:06:19 -0000 1.1 +++ test/org/apache/commons/httpclient/server/SimpleRequest.java 23 Sep 2004 02:45:44 -0000 @@ -31,11 +31,13 @@ package org.apache.commons.httpclient.server; +import java.io.IOException; import java.util.Iterator; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HeaderElement; import org.apache.commons.httpclient.HeaderGroup; +import org.apache.commons.httpclient.HttpConstants; import org.apache.commons.httpclient.NameValuePair; /** @@ -48,7 +50,7 @@ private RequestLine requestLine = null; private String contentType = "text/plain"; private String charSet = null; - private String bodyString = null; + private byte[] body = null; private HeaderGroup headers = new HeaderGroup(); public SimpleRequest() { @@ -58,13 +60,14 @@ public SimpleRequest( final RequestLine requestLine, final Header[] headers, - final String bodyString) + final byte[] body) { super(); if (requestLine == null) { throw new IllegalArgumentException("Request line may not be null"); } this.requestLine = requestLine; + this.body = body; if (headers != null) { this.headers.setHeaders(headers); Header content = this.headers.getFirstHeader("Content-Type"); @@ -79,9 +82,6 @@ } } } - this.bodyString = bodyString; - - } public String getContentType() { @@ -92,8 +92,12 @@ return this.charSet; } - public String getBodyString() { - return this.bodyString; + public byte[] getBody() { + return body; + } + + public String getBodyString() throws IOException { + return new String(body, charSet == null ? HttpConstants.DEFAULT_CONTENT_CHARSET : charSet); } public RequestLine getRequestLine() { Index: java/org/apache/commons/httpclient/methods/multipart/MultipartRequestEntity.java =================================================================== RCS file: java/org/apache/commons/httpclient/methods/multipart/MultipartRequestEntity.java diff -N java/org/apache/commons/httpclient/methods/multipart/MultipartRequestEntity.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ java/org/apache/commons/httpclient/methods/multipart/MultipartRequestEntity.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,128 @@ +/* + * $Header: $ + * $Revision: $ + * $Date: $ + * + * ==================================================================== + * + * Copyright 2002-2004 The Apache Software Foundation + * + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.commons.httpclient.methods.multipart; + +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.httpclient.methods.RequestEntity; +import org.apache.commons.httpclient.params.HttpMethodParams; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implements a request entity suitable for an HTTP multipart POST method. + *

+ * The HTTP multipart POST method is defined in section 3.3 of + * RFC1867: + *

+ * The media-type multipart/form-data follows the rules of all multipart + * MIME data streams as outlined in RFC 1521. The multipart/form-data contains + * a series of parts. Each part is expected to contain a content-disposition + * header where the value is "form-data" and a name attribute specifies + * the field name within the form, e.g., 'content-disposition: form-data; + * name="xxxxx"', where xxxxx is the field name corresponding to that field. + * Field names originally in non-ASCII character sets may be encoded using + * the method outlined in RFC 1522. + *
+ *

+ * + * @since 3.0 + */ +public class MultipartRequestEntity implements RequestEntity { + + private static final Log log = LogFactory.getLog(MultipartRequestEntity.class); + + /** The Content-Type for multipart/form-data. */ + private static final String MULTIPART_FORM_CONTENT_TYPE = "multipart/form-data"; + + private Part[] parts; + + private HttpMethodParams params; + + /** + * + */ + public MultipartRequestEntity(Part[] parts, HttpMethodParams params) { + if (parts == null) { + throw new IllegalArgumentException("parts cannot be null"); + } + if (params == null) { + throw new IllegalArgumentException("params cannot be null"); + } + this.parts = parts; + this.params = params; + } + + /** + * Returns true if all parts are repeatable, false otherwise. + * @see org.apache.commons.httpclient.methods.RequestEntity#isRepeatable() + */ + public boolean isRepeatable() { + for (int i = 0; i < parts.length; i++) { + if (!parts[i].isRepeatable()) { + return false; + } + } + return true; + } + + /* (non-Javadoc) + * @see org.apache.commons.httpclient.methods.RequestEntity#writeRequest(java.io.OutputStream) + */ + public void writeRequest(OutputStream out) throws IOException { + Part.sendParts(out, parts); + } + + /* (non-Javadoc) + * @see org.apache.commons.httpclient.methods.RequestEntity#getContentLength() + */ + public long getContentLength() { + try { + return Part.getLengthOfParts(parts); + } catch (Exception e) { + log.error("An exception occurred while getting the length of the parts", e); + return 0; + } + } + + /* (non-Javadoc) + * @see org.apache.commons.httpclient.methods.RequestEntity#getContentType() + */ + public String getContentType() { + StringBuffer buffer = new StringBuffer(MULTIPART_FORM_CONTENT_TYPE); + String boundary = (String) params.getParameter(HttpMethodParams.MULTIPART_BOUNDARY); + if (boundary != null) { + buffer.append("; boundary="); + buffer.append(boundary); + } + return buffer.toString(); + } + +} Index: test/org/apache/commons/httpclient/EchoService.java =================================================================== RCS file: test/org/apache/commons/httpclient/EchoService.java diff -N test/org/apache/commons/httpclient/EchoService.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ test/org/apache/commons/httpclient/EchoService.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,61 @@ +/* + * $Header: $ + * $Revision: $ + * $Date: $ + * + * ==================================================================== + * + * Copyright 2002-2004 The Apache Software Foundation + * + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.commons.httpclient; + +import java.io.IOException; + +import org.apache.commons.httpclient.server.HttpService; +import org.apache.commons.httpclient.server.SimpleRequest; +import org.apache.commons.httpclient.server.SimpleResponse; + + +/** + * A service that echos the request body. + */ +class EchoService implements HttpService { + + public EchoService() { + super(); + } + + public boolean process(final SimpleRequest request, final SimpleResponse response) + throws IOException + { + HttpVersion httpversion = request.getRequestLine().getHttpVersion(); + response.setStatusLine(httpversion, HttpStatus.SC_OK); + if (request.containsHeader("Content-Length")) { + response.addHeader(request.getFirstHeader("Content-Length")); + } + if (request.containsHeader("Content-Type")) { + response.addHeader(request.getFirstHeader("Content-Type")); + } + response.setBodyString(request.getBodyString()); + return true; + } +} Index: test/org/apache/commons/httpclient/SimpleChunkedInputStream.java =================================================================== RCS file: test/org/apache/commons/httpclient/SimpleChunkedInputStream.java diff -N test/org/apache/commons/httpclient/SimpleChunkedInputStream.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ test/org/apache/commons/httpclient/SimpleChunkedInputStream.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,351 @@ +/* + * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ChunkedInputStream.java,v 1.22 2004/04/18 23:51:34 jsdever Exp $ + * $Revision: 1.22 $ + * $Date: 2004/04/18 23:51:34 $ + * + * ==================================================================== + * + * Copyright 2002-2004 The Apache Software Foundation + * + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.commons.httpclient; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.httpclient.util.EncodingUtil; +import org.apache.commons.httpclient.util.ExceptionUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + *

Transparently coalesces chunks of a HTTP stream that uses + * Transfer-Encoding chunked.

+ * + *

Note that this class NEVER closes the underlying stream, even when close + * gets called. Instead, it will read until the "end" of its chunking on close, + * which allows for the seamless invocation of subsequent HTTP 1.1 calls, while + * not requiring the client to remember to read the entire contents of the + * response.

+ * + * @author Ortwin Gl?ck + * @author Sean C. Sullivan + * @author Martin Elwin + * @author Eric Johnson + * @author Mike Bowler + * @author Michael Becke + * @author Oleg Kalnichevski + * + * @since 2.0 + * + */ +public class SimpleChunkedInputStream extends InputStream { + /** The inputstream that we're wrapping */ + private InputStream in; + + /** The chunk size */ + private int chunkSize; + + /** The current position within the current chunk */ + private int pos; + + /** True if we'are at the beginning of stream */ + private boolean bof = true; + + /** True if we've reached the end of stream */ + private boolean eof = false; + + /** True if this stream is closed */ + private boolean closed = false; + + private String elementCharset = null; + + /** Log object for this class. */ + private static final Log LOG = LogFactory.getLog(ChunkedInputStream.class); + /** + * + * + * @param in must be non-null + * @param method must be non-null + * + * @throws IOException If an IO error occurs + */ + public SimpleChunkedInputStream( + final InputStream in, final String elementCharset) throws IOException { + + if (in == null) { + throw new IllegalArgumentException("InputStream parameter may not be null"); + } + this.elementCharset = elementCharset; + this.in = in; + this.pos = 0; + } + + /** + *

Returns all the data in a chunked stream in coalesced form. A chunk + * is followed by a CRLF. The method returns -1 as soon as a chunksize of 0 + * is detected.

+ * + *

Trailer headers are read automcatically at the end of the stream and + * can be obtained with the getResponseFooters() method.

+ * + * @return -1 of the end of the stream has been reached or the next data + * byte + * @throws IOException If an IO problem occurs + * + * @see HttpMethod#getResponseFooters() + */ + public int read() throws IOException { + + if (closed) { + throw new IOException("Attempted read from closed stream."); + } + if (eof) { + return -1; + } + if (pos >= chunkSize) { + nextChunk(); + if (eof) { + return -1; + } + } + pos++; + return in.read(); + } + + /** + * Read some bytes from the stream. + * @param b The byte array that will hold the contents from the stream. + * @param off The offset into the byte array at which bytes will start to be + * placed. + * @param len the maximum number of bytes that can be returned. + * @return The number of bytes returned or -1 if the end of stream has been + * reached. + * @see java.io.InputStream#read(byte[], int, int) + * @throws IOException if an IO problem occurs. + */ + public int read (byte[] b, int off, int len) throws IOException { + + if (closed) { + throw new IOException("Attempted read from closed stream."); + } + + if (eof) { + return -1; + } + if (pos >= chunkSize) { + nextChunk(); + if (eof) { + return -1; + } + } + len = Math.min(len, chunkSize - pos); + int count = in.read(b, off, len); + pos += count; + return count; + } + + /** + * Read some bytes from the stream. + * @param b The byte array that will hold the contents from the stream. + * @return The number of bytes returned or -1 if the end of stream has been + * reached. + * @see java.io.InputStream#read(byte[]) + * @throws IOException if an IO problem occurs. + */ + public int read (byte[] b) throws IOException { + return read(b, 0, b.length); + } + + /** + * Read the CRLF terminator. + * @throws IOException If an IO error occurs. + */ + private void readCRLF() throws IOException { + int cr = in.read(); + int lf = in.read(); + if ((cr != '\r') || (lf != '\n')) { + throw new IOException( + "CRLF expected at end of chunk: " + cr + "/" + lf); + } + } + + + /** + * Read the next chunk. + * @throws IOException If an IO error occurs. + */ + private void nextChunk() throws IOException { + if (!bof) { + readCRLF(); + } + chunkSize = getChunkSizeFromInputStream(in); + bof = false; + pos = 0; + if (chunkSize == 0) { + eof = true; + parseTrailerHeaders(); + } + } + + /** + * Expects the stream to start with a chunksize in hex with optional + * comments after a semicolon. The line must end with a CRLF: "a3; some + * comment\r\n" Positions the stream at the start of the next line. + * + * @param in The new input stream. + * @param required true if a valid chunk must be present, + * false otherwise. + * + * @return the chunk size as integer + * + * @throws IOException when the chunk size could not be parsed + */ + private static int getChunkSizeFromInputStream(final InputStream in) + throws IOException { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + // States: 0=normal, 1=\r was scanned, 2=inside quoted string, -1=end + int state = 0; + while (state != -1) { + int b = in.read(); + if (b == -1) { + throw new IOException("chunked stream ended unexpectedly"); + } + switch (state) { + case 0: + switch (b) { + case '\r': + state = 1; + break; + case '\"': + state = 2; + /* fall through */ + default: + baos.write(b); + } + break; + + case 1: + if (b == '\n') { + state = -1; + } else { + // this was not CRLF + throw new IOException("Protocol violation: Unexpected" + + " single newline character in chunk size"); + } + break; + + case 2: + switch (b) { + case '\\': + b = in.read(); + baos.write(b); + break; + case '\"': + state = 0; + /* fall through */ + default: + baos.write(b); + } + break; + default: throw new RuntimeException("assertion failed"); + } + } + + //parse data + String dataString = EncodingUtil.getAsciiString(baos.toByteArray()); + int separator = dataString.indexOf(';'); + dataString = (separator > 0) + ? dataString.substring(0, separator).trim() + : dataString.trim(); + + int result; + try { + result = Integer.parseInt(dataString.trim(), 16); + } catch (NumberFormatException e) { + throw new IOException ("Bad chunk size: " + dataString); + } + return result; + } + + /** + * Reads and stores the Trailer headers. + * @throws IOException If an IO problem occurs + */ + private void parseTrailerHeaders() throws IOException { + try { + // ignore trailer headers + while (true) { + String line = HttpParser.readLine(in, elementCharset); + if ((line == null) || (line.length() < 1)) { + break; + } + } + } catch(HttpException e) { + LOG.error("Error parsing trailer headers", e); + IOException ioe = new IOException(e.getMessage()); + ExceptionUtil.initCause(ioe, e); + throw ioe; + } + } + + /** + * Upon close, this reads the remainder of the chunked message, + * leaving the underlying socket at a position to start reading the + * next response without scanning. + * @throws IOException If an IO problem occurs. + */ + public void close() throws IOException { + if (!closed) { + try { + if (!eof) { + exhaustInputStream(this); + } + } finally { + eof = true; + closed = true; + } + } + } + + /** + * Exhaust an input stream, reading until EOF has been encountered. + * + *

Note that this function is intended as a non-public utility. + * This is a little weird, but it seemed silly to make a utility + * class for this one function, so instead it is just static and + * shared that way.

+ * + * @param inStream The {@link InputStream} to exhaust. + * @throws IOException If an IO problem occurs + */ + static void exhaustInputStream(InputStream inStream) throws IOException { + // read and discard the remainder of the message + byte buffer[] = new byte[1024]; + while (inStream.read(buffer) >= 0) { + ; + } + } +} Index: test/org/apache/commons/httpclient/TestMultipartPost.java =================================================================== RCS file: test/org/apache/commons/httpclient/TestMultipartPost.java diff -N test/org/apache/commons/httpclient/TestMultipartPost.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ test/org/apache/commons/httpclient/TestMultipartPost.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,119 @@ +/* + * $Header: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestWebappMultiPostMethod.java,v 1.5 2004/02/22 18:08:50 olegk Exp $ + * $Revision: 1.5 $ + * $Date: 2004/02/22 18:08:50 $ + * + * ==================================================================== + * + * Copyright 2003-2004 The Apache Software Foundation + * + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + * [Additional notices, if required by prior licensing conditions] + * + */ + +package org.apache.commons.httpclient; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.multipart.ByteArrayPartSource; +import org.apache.commons.httpclient.methods.multipart.FilePart; +import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity; +import org.apache.commons.httpclient.methods.multipart.Part; +import org.apache.commons.httpclient.methods.multipart.StringPart; + +/** + * Webapp tests specific to the MultiPostMethod. + * + * @author Oleg Kalnichevski + */ +public class TestMultipartPost extends HttpClientTestBase { + + public TestMultipartPost(String testName) { + super(testName); + } + + public static Test suite() { + TestSuite suite = new TestSuite(TestMultipartPost.class); + return suite; + } + + public static void main(String args[]) { + String[] testCaseName = { TestMultipartPost.class.getName() }; + junit.textui.TestRunner.main(testCaseName); + } + + // ------------------------------------------------------------------ Tests + + /** + * Test that the body consisting of a string part can be posted. + */ + public void testPostStringPart() throws Exception { + + this.server.setHttpService(new EchoService()); + + PostMethod method = new PostMethod(); + MultipartRequestEntity entity = new MultipartRequestEntity( + new Part[] { new StringPart("param", "Hello", "ISO-8859-1") }, + method.getParams()); + method.setRequestEntity(entity); + client.executeMethod(method); + + assertEquals(200,method.getStatusCode()); + String body = method.getResponseBodyAsString(); + assertTrue(body.indexOf("Content-Disposition: form-data; name=\"param\"") >= 0); + assertTrue(body.indexOf("Content-Type: text/plain; charset=ISO-8859-1") >= 0); + assertTrue(body.indexOf("Content-Transfer-Encoding: 8bit") >= 0); + assertTrue(body.indexOf("Hello") >= 0); + } + + + /** + * Test that the body consisting of a file part can be posted. + */ + public void testPostFilePart() throws Exception { + + this.server.setHttpService(new EchoService()); + + PostMethod method = new PostMethod(); + byte[] content = "Hello".getBytes(); + MultipartRequestEntity entity = new MultipartRequestEntity( + new Part[] { + new FilePart( + "param1", + new ByteArrayPartSource("filename.txt", content), + "text/plain", + "ISO-8859-1") }, + method.getParams()); + method.setRequestEntity(entity); + + client.executeMethod(method); + + assertEquals(200,method.getStatusCode()); + String body = method.getResponseBodyAsString(); + assertTrue(body.indexOf("Content-Disposition: form-data; name=\"param1\"; filename=\"filename.txt\"") >= 0); + assertTrue(body.indexOf("Content-Type: text/plain; charset=ISO-8859-1") >= 0); + assertTrue(body.indexOf("Content-Transfer-Encoding: binary") >= 0); + assertTrue(body.indexOf("Hello") >= 0); + } +} +