diff --git a/module-httpmime/pom.xml b/module-httpmime/pom.xml index ecb8be093..0669afc33 100644 --- a/module-httpmime/pom.xml +++ b/module-httpmime/pom.xml @@ -69,24 +69,11 @@ apache-mime4j ${mime4j.version} - - commons-io - commons-io - ${commons-io.version} - commons-logging commons-logging ${commons-logging.version} - - - log4j - log4j - 1.2.14 - junit junit diff --git a/module-httpmime/src/main/java/org/apache/http/entity/mime/ContentDescriptor.java b/module-httpmime/src/main/java/org/apache/http/entity/mime/ContentDescriptor.java deleted file mode 100644 index 5296f9894..000000000 --- a/module-httpmime/src/main/java/org/apache/http/entity/mime/ContentDescriptor.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * $HeadURL$ - * $Revision$ - * $Date$ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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.http.entity.mime; - -import java.nio.charset.Charset; - -public interface ContentDescriptor { - - String getTransferEncoding(); - - String getMimeType(); - - Charset getCharset(); - - long getContentLength(); - -} diff --git a/module-httpmime/src/main/java/org/apache/http/entity/mime/FormBodyPart.java b/module-httpmime/src/main/java/org/apache/http/entity/mime/FormBodyPart.java index 09e18f881..f4d28a245 100644 --- a/module-httpmime/src/main/java/org/apache/http/entity/mime/FormBodyPart.java +++ b/module-httpmime/src/main/java/org/apache/http/entity/mime/FormBodyPart.java @@ -32,6 +32,7 @@ package org.apache.http.entity.mime; import org.apache.http.entity.mime.content.ContentBody; +import org.apache.james.mime4j.ContentDescriptor; import org.apache.james.mime4j.field.Field; import org.apache.james.mime4j.message.BodyPart; import org.apache.james.mime4j.message.Header; @@ -57,7 +58,7 @@ public class FormBodyPart extends BodyPart { } this.name = name; - Header header = new RFC822Header(); + Header header = new Header(); setHeader(header); setBody(body); @@ -92,7 +93,7 @@ public class FormBodyPart extends BodyPart { buffer.append(desc.getMimeType()); if (desc.getCharset() != null) { buffer.append("; charset="); - buffer.append(desc.getCharset().name()); + buffer.append(desc.getCharset()); } getHeader().addField(Field.parse(buffer.toString())); } diff --git a/module-httpmime/src/main/java/org/apache/http/entity/mime/HttpMultipart.java b/module-httpmime/src/main/java/org/apache/http/entity/mime/HttpMultipart.java index 8f39cf01c..98780f1db 100644 --- a/module-httpmime/src/main/java/org/apache/http/entity/mime/HttpMultipart.java +++ b/module-httpmime/src/main/java/org/apache/http/entity/mime/HttpMultipart.java @@ -32,15 +32,16 @@ package org.apache.http.entity.mime; import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.List; -import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.http.entity.mime.content.ContentBody; import org.apache.http.protocol.HTTP; +import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.field.ContentTypeField; import org.apache.james.mime4j.field.Field; import org.apache.james.mime4j.message.Body; @@ -48,6 +49,7 @@ import org.apache.james.mime4j.message.BodyPart; import org.apache.james.mime4j.message.Entity; import org.apache.james.mime4j.message.Multipart; import org.apache.james.mime4j.util.CharsetUtil; +import org.apache.james.mime4j.util.MessageUtils; /** * An extension of the mime4j standard {@link Multipart} class, which is @@ -60,8 +62,8 @@ public class HttpMultipart extends Multipart { private HttpMultipartMode mode; - public HttpMultipart() { - super(); + public HttpMultipart(final String subType) { + super(subType); this.mode = HttpMultipartMode.STRICT; } @@ -101,7 +103,10 @@ public class HttpMultipart extends Multipart { return cField.getBoundary(); } - private void writeTo(final OutputStream out, boolean writeContent) throws IOException { + private void doWriteTo( + final HttpMultipartMode mode, + final OutputStream out, + boolean writeContent) throws IOException { List bodyParts = getBodyParts(); Charset charset = getCharset(); @@ -111,10 +116,13 @@ public class HttpMultipart extends Multipart { new OutputStreamWriter(out, charset), 8192); - switch (this.mode) { + switch (mode) { case STRICT: - writer.write(getPreamble()); - writer.write("\r\n"); + String preamble = getPreamble(); + if (preamble != null && preamble.length() != 0) { + writer.write(preamble); + writer.write("\r\n"); + } for (int i = 0; i < bodyParts.size(); i++) { writer.write("--"); @@ -122,9 +130,9 @@ public class HttpMultipart extends Multipart { writer.write("\r\n"); writer.flush(); BodyPart part = (BodyPart) bodyParts.get(i); - part.getHeader().writeTo(out); + part.getHeader().writeTo(out, MessageUtils.STRICT_IGNORE); if (writeContent) { - part.getBody().writeTo(out); + part.getBody().writeTo(out, MessageUtils.STRICT_IGNORE); } writer.write("\r\n"); } @@ -132,8 +140,11 @@ public class HttpMultipart extends Multipart { writer.write("--"); writer.write(boundary); writer.write("--\r\n"); - writer.write(getEpilogue()); - writer.write("\r\n"); + String epilogue = getEpilogue(); + if (epilogue != null && epilogue.length() != 0) { + writer.write(epilogue); + writer.write("\r\n"); + } writer.flush(); break; case BROWSER_COMPATIBLE: @@ -142,8 +153,6 @@ public class HttpMultipart extends Multipart { // (2) Only write Content-Disposition // (3) Use content charset - writer.write("\r\n"); - for (int i = 0; i < bodyParts.size(); i++) { writer.write("--"); writer.write(boundary); @@ -157,7 +166,7 @@ public class HttpMultipart extends Multipart { writer.write("\r\n"); writer.flush(); if (writeContent) { - part.getBody().writeTo(out); + part.getBody().writeTo(out, MessageUtils.LENIENT); } writer.write("\r\n"); @@ -166,7 +175,6 @@ public class HttpMultipart extends Multipart { writer.write("--"); writer.write(boundary); writer.write("--\r\n"); - writer.write("\r\n"); writer.flush(); break; } @@ -179,9 +187,17 @@ public class HttpMultipart extends Multipart { * * @see #getMode() */ - @Override public void writeTo(final OutputStream out) throws IOException { - writeTo(out, true); + doWriteTo(this.mode, out, true); + } + + @Override + public void writeTo(final OutputStream out, int mode) throws IOException, MimeException { + if (mode == MessageUtils.LENIENT) { + doWriteTo(HttpMultipartMode.BROWSER_COMPATIBLE, out, true); + } else { + doWriteTo(HttpMultipartMode.STRICT, out, true); + } } /** @@ -218,7 +234,7 @@ public class HttpMultipart extends Multipart { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { - writeTo(out, false); + doWriteTo(this.mode, out, false); byte[] extra = out.toByteArray(); return contentLen + extra.length; } catch (IOException ex) { diff --git a/module-httpmime/src/main/java/org/apache/http/entity/mime/MultipartEntity.java b/module-httpmime/src/main/java/org/apache/http/entity/mime/MultipartEntity.java index d8b6397f3..843152d52 100644 --- a/module-httpmime/src/main/java/org/apache/http/entity/mime/MultipartEntity.java +++ b/module-httpmime/src/main/java/org/apache/http/entity/mime/MultipartEntity.java @@ -73,14 +73,15 @@ public class MultipartEntity implements HttpEntity { final String boundary, final Charset charset) { super(); - this.multipart = new HttpMultipart(); + this.multipart = new HttpMultipart("form-data"); this.contentType = new BasicHeader( HTTP.CONTENT_TYPE, generateContentType(boundary, charset)); this.dirty = true; Message message = new Message(); - org.apache.james.mime4j.message.Header header = new RFC822Header(); + org.apache.james.mime4j.message.Header header = + new org.apache.james.mime4j.message.Header(); header.addField( Field.parse("Content-Type: " + this.contentType.getValue())); message.setHeader(header); diff --git a/module-httpmime/src/main/java/org/apache/http/entity/mime/RFC822Header.java b/module-httpmime/src/main/java/org/apache/http/entity/mime/RFC822Header.java deleted file mode 100644 index 3e2d34e4a..000000000 --- a/module-httpmime/src/main/java/org/apache/http/entity/mime/RFC822Header.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * $HeadURL$ - * $Revision$ - * $Date$ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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.http.entity.mime; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.util.Iterator; - -import org.apache.james.mime4j.message.Header; - -/** - * {@link Header} implementation with the stricter RDC 822 compliance. - * To be removed if resolved in mime4j. - */ -class RFC822Header extends Header { - - @Override - public void writeTo(final OutputStream out) throws IOException { - BufferedWriter writer = new BufferedWriter( - new OutputStreamWriter(out, MIME.DEFAULT_CHARSET), 8192); - for (Iterator it = getFields().iterator(); it.hasNext();) { - writer.write(it.next().toString()); - writer.write("\r\n"); - } - writer.write("\r\n"); - writer.flush(); - } - -} diff --git a/module-httpmime/src/main/java/org/apache/http/entity/mime/content/ContentBody.java b/module-httpmime/src/main/java/org/apache/http/entity/mime/content/ContentBody.java index a876dc9c0..4cd62dc80 100644 --- a/module-httpmime/src/main/java/org/apache/http/entity/mime/content/ContentBody.java +++ b/module-httpmime/src/main/java/org/apache/http/entity/mime/content/ContentBody.java @@ -31,7 +31,7 @@ package org.apache.http.entity.mime.content; -import org.apache.http.entity.mime.ContentDescriptor; +import org.apache.james.mime4j.ContentDescriptor; import org.apache.james.mime4j.message.Body; public interface ContentBody extends Body, ContentDescriptor { diff --git a/module-httpmime/src/main/java/org/apache/http/entity/mime/content/FileBody.java b/module-httpmime/src/main/java/org/apache/http/entity/mime/content/FileBody.java index 71d2b8274..3aedbe801 100644 --- a/module-httpmime/src/main/java/org/apache/http/entity/mime/content/FileBody.java +++ b/module-httpmime/src/main/java/org/apache/http/entity/mime/content/FileBody.java @@ -36,9 +36,9 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Map; -import org.apache.commons.io.IOUtils; import org.apache.http.entity.mime.MIME; import org.apache.james.mime4j.message.AbstractBody; import org.apache.james.mime4j.message.BinaryBody; @@ -59,13 +59,18 @@ public class FileBody extends AbstractBody implements BinaryBody, ContentBody { return new FileInputStream(this.file); } - public void writeTo(final OutputStream out) throws IOException { + public void writeTo(final OutputStream out, int mode) throws IOException { if (out == null) { throw new IllegalArgumentException("Output stream may not be null"); } InputStream in = new FileInputStream(this.file); try { - IOUtils.copy(in, out); + byte[] tmp = new byte[4096]; + int l; + while ((l = in.read(tmp)) != -1) { + out.write(tmp, 0, l); + } + out.flush(); } finally { in.close(); } @@ -75,7 +80,7 @@ public class FileBody extends AbstractBody implements BinaryBody, ContentBody { return MIME.ENC_BINARY; } - public Charset getCharset() { + public String getCharset() { return null; } @@ -83,6 +88,18 @@ public class FileBody extends AbstractBody implements BinaryBody, ContentBody { return "application/octet-stream"; } + public Map getContentTypeParameters() { + return Collections.EMPTY_MAP; + } + + public String getMediaType() { + return "application"; + } + + public String getSubType() { + return "octet-stream"; + } + public long getContentLength() { return this.file.length(); } diff --git a/module-httpmime/src/main/java/org/apache/http/entity/mime/content/InputStreamBody.java b/module-httpmime/src/main/java/org/apache/http/entity/mime/content/InputStreamBody.java index 7ca255231..b241c3cd5 100644 --- a/module-httpmime/src/main/java/org/apache/http/entity/mime/content/InputStreamBody.java +++ b/module-httpmime/src/main/java/org/apache/http/entity/mime/content/InputStreamBody.java @@ -34,9 +34,9 @@ package org.apache.http.entity.mime.content; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Map; -import org.apache.commons.io.IOUtils; import org.apache.http.entity.mime.MIME; import org.apache.james.mime4j.message.AbstractBody; import org.apache.james.mime4j.message.BinaryBody; @@ -59,12 +59,17 @@ public class InputStreamBody extends AbstractBody implements BinaryBody, Content return this.in; } - public void writeTo(final OutputStream out) throws IOException { + public void writeTo(final OutputStream out, int mode) throws IOException { if (out == null) { throw new IllegalArgumentException("Output stream may not be null"); } try { - IOUtils.copy(this.in, out); + byte[] tmp = new byte[4096]; + int l; + while ((l = this.in.read(tmp)) != -1) { + out.write(tmp, 0, l); + } + out.flush(); } finally { this.in.close(); } @@ -74,7 +79,7 @@ public class InputStreamBody extends AbstractBody implements BinaryBody, Content return MIME.ENC_BINARY; } - public Charset getCharset() { + public String getCharset() { return null; } @@ -82,6 +87,18 @@ public class InputStreamBody extends AbstractBody implements BinaryBody, Content return "application/octet-stream"; } + public Map getContentTypeParameters() { + return Collections.EMPTY_MAP; + } + + public String getMediaType() { + return "application"; + } + + public String getSubType() { + return "octet-stream"; + } + public long getContentLength() { return -1; } diff --git a/module-httpmime/src/main/java/org/apache/http/entity/mime/content/StringBody.java b/module-httpmime/src/main/java/org/apache/http/entity/mime/content/StringBody.java index 8fb98a61b..bf4d79bab 100644 --- a/module-httpmime/src/main/java/org/apache/http/entity/mime/content/StringBody.java +++ b/module-httpmime/src/main/java/org/apache/http/entity/mime/content/StringBody.java @@ -33,13 +33,15 @@ package org.apache.http.entity.mime.content; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; -import org.apache.commons.io.IOUtils; import org.apache.http.entity.mime.MIME; import org.apache.james.mime4j.message.AbstractBody; import org.apache.james.mime4j.message.TextBody; @@ -71,25 +73,45 @@ public class StringBody extends AbstractBody implements TextBody, ContentBody { this.charset); } - public void writeTo(final OutputStream out) throws IOException { + public void writeTo(final OutputStream out, int mode) throws IOException { if (out == null) { throw new IllegalArgumentException("Output stream may not be null"); } - IOUtils.copy(new ByteArrayInputStream(this.content), out); + InputStream in = new ByteArrayInputStream(this.content); + byte[] tmp = new byte[4096]; + int l; + while ((l = in.read(tmp)) != -1) { + out.write(tmp, 0, l); + } + out.flush(); } public String getTransferEncoding() { return MIME.ENC_8BIT; } - public Charset getCharset() { - return this.charset; + public String getCharset() { + return this.charset.name(); } public String getMimeType() { return "text/plain"; } + public String getMediaType() { + return "text"; + } + + public String getSubType() { + return "plain"; + } + + public Map getContentTypeParameters() { + Map map = new HashMap(); + map.put("charset", this.charset.name()); + return map; + } + public long getContentLength() { return this.content.length; } diff --git a/module-httpmime/src/test/java/org/apache/http/entity/mime/TestMultipartForm.java b/module-httpmime/src/test/java/org/apache/http/entity/mime/TestMultipartForm.java index edf97ee13..12d1d9803 100644 --- a/module-httpmime/src/test/java/org/apache/http/entity/mime/TestMultipartForm.java +++ b/module-httpmime/src/test/java/org/apache/http/entity/mime/TestMultipartForm.java @@ -80,7 +80,7 @@ public class TestMultipartForm extends TestCase { Field.parse("Content-Type: multipart/form-data; boundary=foo")); message.setHeader(header); - HttpMultipart multipart = new HttpMultipart(); + HttpMultipart multipart = new HttpMultipart("form-data"); multipart.setParent(message); BodyPart p1 = new BodyPart(); Header h1 = new Header(); @@ -106,7 +106,7 @@ public class TestMultipartForm extends TestCase { multipart.writeTo(out); out.close(); - String expected = "\r\n" + + String expected = "--foo\r\n" + "Content-Type: text/plain\r\n" + "\r\n" + @@ -119,8 +119,7 @@ public class TestMultipartForm extends TestCase { "Content-Type: text/plain\r\n" + "\r\n" + "all kind of stuff\r\n" + - "--foo--\r\n" + - "\r\n"; + "--foo--\r\n"; String s = out.toString("US-ASCII"); assertEquals(expected, s); assertEquals(s.length(), multipart.getTotalLength()); @@ -133,7 +132,7 @@ public class TestMultipartForm extends TestCase { Field.parse("Content-Type: multipart/form-data; boundary=foo")); message.setHeader(header); - HttpMultipart multipart = new HttpMultipart(); + HttpMultipart multipart = new HttpMultipart("form-data"); multipart.setParent(message); FormBodyPart p1 = new FormBodyPart( "field1", @@ -153,7 +152,7 @@ public class TestMultipartForm extends TestCase { multipart.writeTo(out); out.close(); - String expected = "\r\n" + + String expected = "--foo\r\n" + "Content-Disposition: form-data; name=\"field1\"\r\n" + "Content-Type: text/plain; charset=" + @@ -174,8 +173,7 @@ public class TestMultipartForm extends TestCase { "Content-Transfer-Encoding: 8bit\r\n" + "\r\n" + "all kind of stuff\r\n" + - "--foo--\r\n" + - "\r\n"; + "--foo--\r\n"; String s = out.toString("US-ASCII"); assertEquals(expected, s); assertEquals(s.length(), multipart.getTotalLength()); @@ -197,7 +195,7 @@ public class TestMultipartForm extends TestCase { writer.close(); } - HttpMultipart multipart = new HttpMultipart(); + HttpMultipart multipart = new HttpMultipart("form-data"); multipart.setParent(message); FormBodyPart p1 = new FormBodyPart( "field1", @@ -213,7 +211,7 @@ public class TestMultipartForm extends TestCase { multipart.writeTo(out); out.close(); - String expected = "\r\n" + + String expected = "--foo\r\n" + "Content-Disposition: form-data; name=\"field1\"; " + "filename=\"" + tmpfile.getName() + "\"\r\n" + @@ -228,8 +226,7 @@ public class TestMultipartForm extends TestCase { "Content-Transfer-Encoding: binary\r\n" + "\r\n" + "some random whatever\r\n" + - "--foo--\r\n" + - "\r\n"; + "--foo--\r\n"; String s = out.toString("US-ASCII"); assertEquals(expected, s); assertEquals(-1, multipart.getTotalLength()); @@ -253,7 +250,7 @@ public class TestMultipartForm extends TestCase { writer.close(); } - HttpMultipart multipart = new HttpMultipart(); + HttpMultipart multipart = new HttpMultipart("form-data"); multipart.setParent(message); FormBodyPart p1 = new FormBodyPart( "field1", @@ -271,7 +268,7 @@ public class TestMultipartForm extends TestCase { multipart.writeTo(out); out.close(); - String expected = "\r\n" + + String expected = "--foo\r\n" + "Content-Disposition: form-data; name=\"field1\"; " + "filename=\"" + tmpfile.getName() + "\"\r\n" + @@ -282,8 +279,7 @@ public class TestMultipartForm extends TestCase { "filename=\"file.tmp\"\r\n" + "\r\n" + "some random whatever\r\n" + - "--foo--\r\n" + - "\r\n"; + "--foo--\r\n"; String s = out.toString("US-ASCII"); assertEquals(expected, s); assertEquals(-1, multipart.getTotalLength()); @@ -329,7 +325,7 @@ public class TestMultipartForm extends TestCase { writer.close(); } - HttpMultipart multipart = new HttpMultipart(); + HttpMultipart multipart = new HttpMultipart("form-data"); multipart.setParent(message); FormBodyPart p1 = new FormBodyPart( "field1", @@ -347,7 +343,7 @@ public class TestMultipartForm extends TestCase { multipart.writeTo(out); out.close(); - String expected = "\r\n" + + String expected = "--foo\r\n" + "Content-Disposition: form-data; name=\"field1\"; " + "filename=\"" + s1 + ".tmp\"\r\n" + @@ -358,8 +354,7 @@ public class TestMultipartForm extends TestCase { "filename=\"" + s2 + ".tmp\"\r\n" + "\r\n" + "some random whatever\r\n" + - "--foo--\r\n" + - "\r\n"; + "--foo--\r\n"; String s = out.toString("UTF-8"); assertEquals(expected, s); assertEquals(-1, multipart.getTotalLength()); @@ -377,7 +372,7 @@ public class TestMultipartForm extends TestCase { Field.parse("Content-Type: multipart/form-data; boundary=foo")); message.setHeader(header); - HttpMultipart multipart = new HttpMultipart(); + HttpMultipart multipart = new HttpMultipart("form-data"); multipart.setParent(message); FormBodyPart p1 = new FormBodyPart( "field1", @@ -395,7 +390,7 @@ public class TestMultipartForm extends TestCase { ByteArrayOutputStream out2 = new ByteArrayOutputStream(); - out2.write(("\r\n" + + out2.write(( "--foo\r\n" + "Content-Disposition: form-data; name=\"field1\"\r\n" + "Content-Type: text/plain; charset=ISO-8859-1\r\n" + @@ -410,8 +405,7 @@ public class TestMultipartForm extends TestCase { "\r\n").getBytes("US-ASCII")); out2.write(s2.getBytes("KOI8-R")); out2.write(("\r\n" + - "--foo--\r\n" + - "\r\n").getBytes("US-ASCII")); + "--foo--\r\n").getBytes("US-ASCII")); out2.close(); byte[] actual = out1.toByteArray(); diff --git a/pom.xml b/pom.xml index bbe6456c5..dd15830c1 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 1.1.1 1.3 1.2 - 0.3 + 0.4-SNAPSHOT 3.8.2