From 36ac52a4bdfc4116e03e798780cecc4f3a747b46 Mon Sep 17 00:00:00 2001 From: Veebs Date: Sat, 3 Nov 2012 04:17:49 -0700 Subject: [PATCH] Port http multipart package. See #709 --- .../netty/buffer/DefaultCompositeByteBuf.java | 9 +- .../io/netty/buffer/ConsolidationTest.java | 57 + .../http/multipart/AbstractDiskHttpData.java | 346 +++ .../http/multipart/AbstractHttpData.java | 94 + .../multipart/AbstractMemoryHttpData.java | 217 ++ .../codec/http/multipart/Attribute.java | 34 + .../multipart/CaseIgnoringComparator.java | 39 + .../multipart/DefaultHttpDataFactory.java | 184 ++ .../codec/http/multipart/DiskAttribute.java | 144 ++ .../codec/http/multipart/DiskFileUpload.java | 154 ++ .../codec/http/multipart/FileUpload.java | 60 + .../codec/http/multipart/HttpData.java | 181 ++ .../codec/http/multipart/HttpDataFactory.java | 80 + .../http/multipart/HttpPostBodyUtil.java | 229 ++ .../multipart/HttpPostRequestDecoder.java | 2124 +++++++++++++++++ .../multipart/HttpPostRequestEncoder.java | 1021 ++++++++ .../http/multipart/InterfaceHttpData.java | 36 + .../http/multipart/InternalAttribute.java | 102 + .../codec/http/multipart/MemoryAttribute.java | 105 + .../http/multipart/MemoryFileUpload.java | 120 + .../codec/http/multipart/MixedAttribute.java | 180 ++ .../codec/http/multipart/MixedFileUpload.java | 201 ++ .../codec/http/multipart/package-info.java | 20 + .../example/http/upload/HttpUploadClient.java | 974 ++++++++ .../http/upload/HttpUploadClientHandler.java | 79 + .../upload/HttpUploadClientIntializer.java | 57 + .../example/http/upload/HttpUploadServer.java | 63 + .../http/upload/HttpUploadServerHandler.java | 427 ++++ .../upload/HttpUploadServerInitializer.java | 52 + 29 files changed, 7382 insertions(+), 7 deletions(-) create mode 100644 buffer/src/test/java/io/netty/buffer/ConsolidationTest.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/Attribute.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/CaseIgnoringComparator.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/DefaultHttpDataFactory.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/FileUpload.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpData.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpDataFactory.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostBodyUtil.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/InterfaceHttpData.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedAttribute.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedFileUpload.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/multipart/package-info.java create mode 100644 example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java create mode 100644 example/src/main/java/io/netty/example/http/upload/HttpUploadClientHandler.java create mode 100644 example/src/main/java/io/netty/example/http/upload/HttpUploadClientIntializer.java create mode 100644 example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java create mode 100644 example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java create mode 100644 example/src/main/java/io/netty/example/http/upload/HttpUploadServerInitializer.java diff --git a/buffer/src/main/java/io/netty/buffer/DefaultCompositeByteBuf.java b/buffer/src/main/java/io/netty/buffer/DefaultCompositeByteBuf.java index 646bf33d0d..202921f2d0 100644 --- a/buffer/src/main/java/io/netty/buffer/DefaultCompositeByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/DefaultCompositeByteBuf.java @@ -227,14 +227,9 @@ public class DefaultCompositeByteBuf extends AbstractByteBuf implements Composit if (b == null) { break; } - if (b.readable()) { - addComponent(cIndex ++, b); - int size = components.size(); - if (cIndex > size) { - // was consolidated, so adjust index. #707 - cIndex = size; - } + // Always add b to the end of the component list because we are appending buffers + addComponent(components.size(), b); } } return this; diff --git a/buffer/src/test/java/io/netty/buffer/ConsolidationTest.java b/buffer/src/test/java/io/netty/buffer/ConsolidationTest.java new file mode 100644 index 0000000000..57eb439cc1 --- /dev/null +++ b/buffer/src/test/java/io/netty/buffer/ConsolidationTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.buffer; + +import static io.netty.buffer.Unpooled.wrappedBuffer; + +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * Tests buffer consolidation + */ +public class ConsolidationTest { + + @Test + public void shouldWrapInSequence() { + ByteBuf currentBuffer = wrappedBuffer(wrappedBuffer("a".getBytes()), wrappedBuffer("=".getBytes())); + currentBuffer = wrappedBuffer(currentBuffer, wrappedBuffer("1".getBytes()), wrappedBuffer("&".getBytes())); + + String s = new String(currentBuffer.copy().array()); + assertEquals("a=1&", s); + } + + @Test + public void shouldConsolidationInSequence() { + ByteBuf currentBuffer = wrappedBuffer(wrappedBuffer("a".getBytes()), wrappedBuffer("=".getBytes())); + currentBuffer = wrappedBuffer(currentBuffer, wrappedBuffer("1".getBytes()), wrappedBuffer("&".getBytes())); + + currentBuffer = wrappedBuffer(currentBuffer, wrappedBuffer("b".getBytes()), wrappedBuffer("=".getBytes())); + currentBuffer = wrappedBuffer(currentBuffer, wrappedBuffer("2".getBytes()), wrappedBuffer("&".getBytes())); + + currentBuffer = wrappedBuffer(currentBuffer, wrappedBuffer("c".getBytes()), wrappedBuffer("=".getBytes())); + currentBuffer = wrappedBuffer(currentBuffer, wrappedBuffer("3".getBytes()), wrappedBuffer("&".getBytes())); + + currentBuffer = wrappedBuffer(currentBuffer, wrappedBuffer("d".getBytes()), wrappedBuffer("=".getBytes())); + currentBuffer = wrappedBuffer(currentBuffer, wrappedBuffer("4".getBytes()), wrappedBuffer("&".getBytes())); + + currentBuffer = wrappedBuffer(currentBuffer, wrappedBuffer("e".getBytes()), wrappedBuffer("=".getBytes())); + currentBuffer = wrappedBuffer(currentBuffer, wrappedBuffer("5".getBytes()), wrappedBuffer("&".getBytes())); + + String s = new String(currentBuffer.copy().array()); + assertEquals("a=1&b=2&c=3&d=4&e=5&", s); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java new file mode 100644 index 0000000000..69a2e01518 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java @@ -0,0 +1,346 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import static io.netty.buffer.Unpooled.*; +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpConstants; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +/** + * Abstract Disk HttpData implementation + */ +public abstract class AbstractDiskHttpData extends AbstractHttpData { + + protected File file; + private boolean isRenamed; + private FileChannel fileChannel; + + public AbstractDiskHttpData(String name, Charset charset, long size) { + super(name, charset, size); + } + + /** + * + * @return the real DiskFilename (basename) + */ + protected abstract String getDiskFilename(); + /** + * + * @return the default prefix + */ + protected abstract String getPrefix(); + /** + * + * @return the default base Directory + */ + protected abstract String getBaseDirectory(); + /** + * + * @return the default postfix + */ + protected abstract String getPostfix(); + /** + * + * @return True if the file should be deleted on Exit by default + */ + protected abstract boolean deleteOnExit(); + + /** + * + * @return a new Temp File from getDiskFilename(), default prefix, postfix and baseDirectory + * @throws IOException + */ + private File tempFile() throws IOException { + String newpostfix = null; + String diskFilename = getDiskFilename(); + if (diskFilename != null) { + newpostfix = "_" + diskFilename; + } else { + newpostfix = getPostfix(); + } + File tmpFile; + if (getBaseDirectory() == null) { + // create a temporary file + tmpFile = File.createTempFile(getPrefix(), newpostfix); + } else { + tmpFile = File.createTempFile(getPrefix(), newpostfix, new File( + getBaseDirectory())); + } + if (deleteOnExit()) { + tmpFile.deleteOnExit(); + } + return tmpFile; + } + + public void setContent(ByteBuf buffer) throws IOException { + if (buffer == null) { + throw new NullPointerException("buffer"); + } + size = buffer.readableBytes(); + if (definedSize > 0 && definedSize < size) { + throw new IOException("Out of size: " + size + " > " + definedSize); + } + if (file == null) { + file = tempFile(); + } + if (buffer.readableBytes() == 0) { + // empty file + file.createNewFile(); + return; + } + FileOutputStream outputStream = new FileOutputStream(file); + FileChannel localfileChannel = outputStream.getChannel(); + ByteBuffer byteBuffer = buffer.nioBuffer(); + int written = 0; + while (written < size) { + written += localfileChannel.write(byteBuffer); + } + buffer.readerIndex(buffer.readerIndex() + written); + localfileChannel.force(false); + localfileChannel.close(); + outputStream.close(); + completed = true; + } + + public void addContent(ByteBuf buffer, boolean last) + throws IOException { + if (buffer != null) { + int localsize = buffer.readableBytes(); + if (definedSize > 0 && definedSize < size + localsize) { + throw new IOException("Out of size: " + (size + localsize) + + " > " + definedSize); + } + ByteBuffer byteBuffer = buffer.hasNioBuffer() ? buffer.nioBuffer() : buffer.copy().nioBuffer(); + int written = 0; + if (file == null) { + file = tempFile(); + } + if (fileChannel == null) { + FileOutputStream outputStream = new FileOutputStream(file); + fileChannel = outputStream.getChannel(); + } + while (written < localsize) { + written += fileChannel.write(byteBuffer); + } + size += localsize; + buffer.readerIndex(buffer.readerIndex() + written); + } + if (last) { + if (file == null) { + file = tempFile(); + } + if (fileChannel == null) { + FileOutputStream outputStream = new FileOutputStream(file); + fileChannel = outputStream.getChannel(); + } + fileChannel.force(false); + fileChannel.close(); + fileChannel = null; + completed = true; + } else { + if (buffer == null) { + throw new NullPointerException("buffer"); + } + } + } + + public void setContent(File file) throws IOException { + if (this.file != null) { + delete(); + } + this.file = file; + size = file.length(); + isRenamed = true; + completed = true; + } + + public void setContent(InputStream inputStream) throws IOException { + if (inputStream == null) { + throw new NullPointerException("inputStream"); + } + if (file != null) { + delete(); + } + file = tempFile(); + FileOutputStream outputStream = new FileOutputStream(file); + FileChannel localfileChannel = outputStream.getChannel(); + byte[] bytes = new byte[4096 * 4]; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + int read = inputStream.read(bytes); + int written = 0; + while (read > 0) { + byteBuffer.position(read).flip(); + written += localfileChannel.write(byteBuffer); + read = inputStream.read(bytes); + } + localfileChannel.force(false); + localfileChannel.close(); + size = written; + if (definedSize > 0 && definedSize < size) { + file.delete(); + file = null; + throw new IOException("Out of size: " + size + " > " + definedSize); + } + isRenamed = true; + completed = true; + } + + public void delete() { + if (! isRenamed) { + if (file != null) { + file.delete(); + } + } + } + + public byte[] get() throws IOException { + if (file == null) { + return new byte[0]; + } + return readFrom(file); + } + + public ByteBuf getByteBuf() throws IOException { + if (file == null) { + return EMPTY_BUFFER; + } + byte[] array = readFrom(file); + return wrappedBuffer(array); + } + + public ByteBuf getChunk(int length) throws IOException { + if (file == null || length == 0) { + return EMPTY_BUFFER; + } + if (fileChannel == null) { + FileInputStream inputStream = new FileInputStream(file); + fileChannel = inputStream.getChannel(); + } + int read = 0; + ByteBuffer byteBuffer = ByteBuffer.allocate(length); + while (read < length) { + int readnow = fileChannel.read(byteBuffer); + if (readnow == -1) { + fileChannel.close(); + fileChannel = null; + break; + } else { + read += readnow; + } + } + if (read == 0) { + return EMPTY_BUFFER; + } + byteBuffer.flip(); + ByteBuf buffer = wrappedBuffer(byteBuffer); + buffer.readerIndex(0); + buffer.writerIndex(read); + return buffer; + } + + public String getString() throws IOException { + return getString(HttpConstants.DEFAULT_CHARSET); + } + + public String getString(Charset encoding) throws IOException { + if (file == null) { + return ""; + } + if (encoding == null) { + byte[] array = readFrom(file); + return new String(array, HttpConstants.DEFAULT_CHARSET.name()); + } + byte[] array = readFrom(file); + return new String(array, encoding.name()); + } + + public boolean isInMemory() { + return false; + } + + public boolean renameTo(File dest) throws IOException { + if (dest == null) { + throw new NullPointerException("dest"); + } + if (!file.renameTo(dest)) { + // must copy + FileInputStream inputStream = new FileInputStream(file); + FileOutputStream outputStream = new FileOutputStream(dest); + FileChannel in = inputStream.getChannel(); + FileChannel out = outputStream.getChannel(); + int chunkSize = 8196; + long position = 0; + while (position < size) { + if (chunkSize < size - position) { + chunkSize = (int) (size - position); + } + position += in.transferTo(position, chunkSize , out); + } + in.close(); + out.close(); + if (position == size) { + file.delete(); + file = dest; + isRenamed = true; + return true; + } else { + dest.delete(); + return false; + } + } + file = dest; + isRenamed = true; + return true; + } + + /** + * Utility function + * @param src + * @return the array of bytes + * @throws IOException + */ + private static byte[] readFrom(File src) throws IOException { + long srcsize = src.length(); + if (srcsize > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + "File too big to be loaded in memory"); + } + FileInputStream inputStream = new FileInputStream(src); + FileChannel fileChannel = inputStream.getChannel(); + byte[] array = new byte[(int) srcsize]; + ByteBuffer byteBuffer = ByteBuffer.wrap(array); + int read = 0; + while (read < srcsize) { + read += fileChannel.read(byteBuffer); + } + fileChannel.close(); + return array; + } + + public File getFile() throws IOException { + return file; + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java new file mode 100644 index 0000000000..92dcdfb2e2 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.nio.charset.Charset; + +import io.netty.handler.codec.http.HttpConstants; + +/** + * Abstract HttpData implementation + */ +public abstract class AbstractHttpData implements HttpData { + + protected final String name; + protected long definedSize; + protected long size; + protected Charset charset = HttpConstants.DEFAULT_CHARSET; + protected boolean completed; + + public AbstractHttpData(String name, Charset charset, long size) { + if (name == null) { + throw new NullPointerException("name"); + } + name = name.trim(); + if (name.length() == 0) { + throw new IllegalArgumentException("empty name"); + } + + for (int i = 0; i < name.length(); i ++) { + char c = name.charAt(i); + if (c > 127) { + throw new IllegalArgumentException( + "name contains non-ascii character: " + name); + } + + // Check prohibited characters. + switch (c) { + case '=': + case ',': + case ';': + case ' ': + case '\t': + case '\r': + case '\n': + case '\f': + case 0x0b: // Vertical tab + throw new IllegalArgumentException( + "name contains one of the following prohibited characters: " + + "=,; \\t\\r\\n\\v\\f: " + name); + } + } + this.name = name; + if (charset != null) { + setCharset(charset); + } + definedSize = size; + } + + public String getName() { + return name; + } + + public boolean isCompleted() { + return completed; + } + + public Charset getCharset() { + return charset; + } + + public void setCharset(Charset charset) { + if (charset == null) { + throw new NullPointerException("charset"); + } + this.charset = charset; + } + + public long length() { + return size; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java new file mode 100644 index 0000000000..c02d5f5680 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java @@ -0,0 +1,217 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import static io.netty.buffer.Unpooled.EMPTY_BUFFER; +import static io.netty.buffer.Unpooled.buffer; +import static io.netty.buffer.Unpooled.wrappedBuffer; +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpConstants; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +/** + * Abstract Memory HttpData implementation + */ +public abstract class AbstractMemoryHttpData extends AbstractHttpData { + + private ByteBuf byteBuf; + private int chunkPosition; + protected boolean isRenamed; + + public AbstractMemoryHttpData(String name, Charset charset, long size) { + super(name, charset, size); + } + + public void setContent(ByteBuf buffer) throws IOException { + if (buffer == null) { + throw new NullPointerException("buffer"); + } + long localsize = buffer.readableBytes(); + if (definedSize > 0 && definedSize < localsize) { + throw new IOException("Out of size: " + localsize + " > " + + definedSize); + } + byteBuf = buffer; + size = localsize; + completed = true; + } + + public void setContent(InputStream inputStream) throws IOException { + if (inputStream == null) { + throw new NullPointerException("inputStream"); + } + ByteBuf buffer = buffer(); + byte[] bytes = new byte[4096 * 4]; + int read = inputStream.read(bytes); + int written = 0; + while (read > 0) { + buffer.writeBytes(bytes); + written += read; + read = inputStream.read(bytes); + } + size = written; + if (definedSize > 0 && definedSize < size) { + throw new IOException("Out of size: " + size + " > " + definedSize); + } + byteBuf = buffer; + completed = true; + } + + public void addContent(ByteBuf buffer, boolean last) + throws IOException { + if (buffer != null) { + long localsize = buffer.readableBytes(); + if (definedSize > 0 && definedSize < size + localsize) { + throw new IOException("Out of size: " + (size + localsize) + + " > " + definedSize); + } + size += localsize; + if (byteBuf == null) { + byteBuf = buffer; + } else { + byteBuf = wrappedBuffer(byteBuf, buffer); + } + } + if (last) { + completed = true; + } else { + if (buffer == null) { + throw new NullPointerException("buffer"); + } + } + } + + public void setContent(File file) throws IOException { + if (file == null) { + throw new NullPointerException("file"); + } + long newsize = file.length(); + if (newsize > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + "File too big to be loaded in memory"); + } + FileInputStream inputStream = new FileInputStream(file); + FileChannel fileChannel = inputStream.getChannel(); + byte[] array = new byte[(int) newsize]; + ByteBuffer byteBuffer = ByteBuffer.wrap(array); + int read = 0; + while (read < newsize) { + read += fileChannel.read(byteBuffer); + } + fileChannel.close(); + inputStream.close(); + byteBuffer.flip(); + byteBuf = wrappedBuffer(byteBuffer); + size = newsize; + completed = true; + } + + public void delete() { + // nothing to do + } + + public byte[] get() { + if (byteBuf == null) { + return new byte[0]; + } + byte[] array = new byte[byteBuf.readableBytes()]; + byteBuf.getBytes(byteBuf.readerIndex(), array); + return array; + } + + public String getString() { + return getString(HttpConstants.DEFAULT_CHARSET); + } + + public String getString(Charset encoding) { + if (byteBuf == null) { + return ""; + } + if (encoding == null) { + return getString(HttpConstants.DEFAULT_CHARSET); + } + return byteBuf.toString(encoding); + } + + /** + * Utility to go from a In Memory FileUpload + * to a Disk (or another implementation) FileUpload + * @return the attached ByteBuf containing the actual bytes + */ + public ByteBuf getByteBuf() { + return byteBuf; + } + + public ByteBuf getChunk(int length) throws IOException { + if (byteBuf == null || length == 0 || byteBuf.readableBytes() == 0) { + chunkPosition = 0; + return EMPTY_BUFFER; + } + int sizeLeft = byteBuf.readableBytes() - chunkPosition; + if (sizeLeft == 0) { + chunkPosition = 0; + return EMPTY_BUFFER; + } + int sliceLength = length; + if (sizeLeft < length) { + sliceLength = sizeLeft; + } + ByteBuf chunk = byteBuf.slice(chunkPosition, sliceLength); + chunkPosition += sliceLength; + return chunk; + } + + public boolean isInMemory() { + return true; + } + + public boolean renameTo(File dest) throws IOException { + if (dest == null) { + throw new NullPointerException("dest"); + } + if (byteBuf == null) { + // empty file + dest.createNewFile(); + isRenamed = true; + return true; + } + int length = byteBuf.readableBytes(); + FileOutputStream outputStream = new FileOutputStream(dest); + FileChannel fileChannel = outputStream.getChannel(); + ByteBuffer byteBuffer = byteBuf.nioBuffer(); + int written = 0; + while (written < length) { + written += fileChannel.write(byteBuffer); + } + fileChannel.force(false); + fileChannel.close(); + outputStream.close(); + isRenamed = true; + return written == length; + } + + public File getFile() throws IOException { + throw new IOException("Not represented by a file"); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/Attribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/Attribute.java new file mode 100644 index 0000000000..7263b9f270 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/Attribute.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.io.IOException; + +/** + * Attribute interface + */ +public interface Attribute extends HttpData { + /** + * Returns the value of this HttpData. + */ + String getValue() throws IOException; + + /** + * Sets the value of this HttpData. + * @param value + */ + void setValue(String value) throws IOException; +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/CaseIgnoringComparator.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/CaseIgnoringComparator.java new file mode 100644 index 0000000000..830627e004 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/CaseIgnoringComparator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.io.Serializable; +import java.util.Comparator; + +public final class CaseIgnoringComparator implements Comparator, Serializable { + + private static final long serialVersionUID = 4582133183775373862L; + + public static final CaseIgnoringComparator INSTANCE = new CaseIgnoringComparator(); + + private CaseIgnoringComparator() { + super(); + } + + public int compare(String o1, String o2) { + return o1.compareToIgnoreCase(o2); + } + + @SuppressWarnings("static-method") + private Object readResolve() { + return INSTANCE; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DefaultHttpDataFactory.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DefaultHttpDataFactory.java new file mode 100644 index 0000000000..40dd566a8f --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DefaultHttpDataFactory.java @@ -0,0 +1,184 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import io.netty.handler.codec.http.HttpRequest; + +/** + * Default factory giving Attribute and FileUpload according to constructor + * + * Attribute and FileUpload could be :
+ * - MemoryAttribute, DiskAttribute or MixedAttribute
+ * - MemoryFileUpload, DiskFileUpload or MixedFileUpload
+ * according to the constructor. + */ +public class DefaultHttpDataFactory implements HttpDataFactory { + /** + * Proposed default MINSIZE as 16 KB. + */ + public static long MINSIZE = 0x4000; + + private final boolean useDisk; + + private final boolean checkSize; + + private long minSize; + + /** + * Keep all HttpDatas until cleanAllHttpDatas() is called. + */ + private final ConcurrentHashMap> requestFileDeleteMap = + new ConcurrentHashMap>(); + /** + * HttpData will be in memory if less than default size (16KB). + * The type will be Mixed. + */ + public DefaultHttpDataFactory() { + useDisk = false; + checkSize = true; + minSize = MINSIZE; + } + + /** + * HttpData will be always on Disk if useDisk is True, else always in Memory if False + * @param useDisk + */ + public DefaultHttpDataFactory(boolean useDisk) { + this.useDisk = useDisk; + checkSize = false; + } + + /** + * HttpData will be on Disk if the size of the file is greater than minSize, else it + * will be in memory. The type will be Mixed. + * @param minSize + */ + public DefaultHttpDataFactory(long minSize) { + useDisk = false; + checkSize = true; + this.minSize = minSize; + } + + /** + * + * @param request + * @return the associated list of Files for the request + */ + private List getList(HttpRequest request) { + List list = requestFileDeleteMap.get(request); + if (list == null) { + list = new ArrayList(); + requestFileDeleteMap.put(request, list); + } + return list; + } + + public Attribute createAttribute(HttpRequest request, String name) { + if (useDisk) { + Attribute attribute = new DiskAttribute(name); + List fileToDelete = getList(request); + fileToDelete.add(attribute); + return attribute; + } else if (checkSize) { + Attribute attribute = new MixedAttribute(name, minSize); + List fileToDelete = getList(request); + fileToDelete.add(attribute); + return attribute; + } + return new MemoryAttribute(name); + } + + public Attribute createAttribute(HttpRequest request, String name, String value) { + if (useDisk) { + Attribute attribute; + try { + attribute = new DiskAttribute(name, value); + } catch (IOException e) { + // revert to Mixed mode + attribute = new MixedAttribute(name, value, minSize); + } + List fileToDelete = getList(request); + fileToDelete.add(attribute); + return attribute; + } else if (checkSize) { + Attribute attribute = new MixedAttribute(name, value, minSize); + List fileToDelete = getList(request); + fileToDelete.add(attribute); + return attribute; + } + try { + return new MemoryAttribute(name, value); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + public FileUpload createFileUpload(HttpRequest request, String name, String filename, + String contentType, String contentTransferEncoding, Charset charset, + long size) { + if (useDisk) { + FileUpload fileUpload = new DiskFileUpload(name, filename, contentType, + contentTransferEncoding, charset, size); + List fileToDelete = getList(request); + fileToDelete.add(fileUpload); + return fileUpload; + } else if (checkSize) { + FileUpload fileUpload = new MixedFileUpload(name, filename, contentType, + contentTransferEncoding, charset, size, minSize); + List fileToDelete = getList(request); + fileToDelete.add(fileUpload); + return fileUpload; + } + return new MemoryFileUpload(name, filename, contentType, + contentTransferEncoding, charset, size); + } + + public void removeHttpDataFromClean(HttpRequest request, InterfaceHttpData data) { + if (data instanceof HttpData) { + List fileToDelete = getList(request); + fileToDelete.remove(data); + } + } + + public void cleanRequestHttpDatas(HttpRequest request) { + List fileToDelete = requestFileDeleteMap.remove(request); + if (fileToDelete != null) { + for (HttpData data: fileToDelete) { + data.delete(); + } + fileToDelete.clear(); + } + } + + public void cleanAllHttpDatas() { + for (HttpRequest request : requestFileDeleteMap.keySet()) { + List fileToDelete = requestFileDeleteMap.get(request); + if (fileToDelete != null) { + for (HttpData data: fileToDelete) { + data.delete(); + } + fileToDelete.clear(); + } + requestFileDeleteMap.remove(request); + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java new file mode 100644 index 0000000000..2c3aff2db7 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java @@ -0,0 +1,144 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.io.IOException; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpConstants; +import static io.netty.buffer.Unpooled.*; + +/** + * Disk implementation of Attributes + */ +public class DiskAttribute extends AbstractDiskHttpData implements Attribute { + public static String baseDirectory; + + public static boolean deleteOnExitTemporaryFile = true; + + public static String prefix = "Attr_"; + + public static String postfix = ".att"; + + /** + * Constructor used for huge Attribute + * @param name + */ + public DiskAttribute(String name) { + super(name, HttpConstants.DEFAULT_CHARSET, 0); + } + /** + * + * @param name + * @param value + * @throws NullPointerException + * @throws IllegalArgumentException + * @throws IOException + */ + public DiskAttribute(String name, String value) throws IOException { + super(name, HttpConstants.DEFAULT_CHARSET, 0); // Attribute have no default size + setValue(value); + } + + public HttpDataType getHttpDataType() { + return HttpDataType.Attribute; + } + + public String getValue() throws IOException { + byte [] bytes = get(); + return new String(bytes, charset.name()); + } + + public void setValue(String value) throws IOException { + if (value == null) { + throw new NullPointerException("value"); + } + byte [] bytes = value.getBytes(charset.name()); + ByteBuf buffer = wrappedBuffer(bytes); + if (definedSize > 0) { + definedSize = buffer.readableBytes(); + } + setContent(buffer); + } + + @Override + public void addContent(ByteBuf buffer, boolean last) throws IOException { + int localsize = buffer.readableBytes(); + if (definedSize > 0 && definedSize < size + localsize) { + definedSize = size + localsize; + } + super.addContent(buffer, last); + } + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Attribute)) { + return false; + } + Attribute attribute = (Attribute) o; + return getName().equalsIgnoreCase(attribute.getName()); + } + + public int compareTo(InterfaceHttpData arg0) { + if (!(arg0 instanceof Attribute)) { + throw new ClassCastException("Cannot compare " + getHttpDataType() + + " with " + arg0.getHttpDataType()); + } + return compareTo((Attribute) arg0); + } + + public int compareTo(Attribute o) { + return getName().compareToIgnoreCase(o.getName()); + } + + @Override + public String toString() { + try { + return getName() + "=" + getValue(); + } catch (IOException e) { + return getName() + "=IoException"; + } + } + + @Override + protected boolean deleteOnExit() { + return deleteOnExitTemporaryFile; + } + + @Override + protected String getBaseDirectory() { + return baseDirectory; + } + + @Override + protected String getDiskFilename() { + return getName() + postfix; + } + + @Override + protected String getPostfix() { + return postfix; + } + + @Override + protected String getPrefix() { + return prefix; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java new file mode 100644 index 0000000000..ebca0acf3c --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.io.File; +import java.nio.charset.Charset; + +import io.netty.handler.codec.http.HttpHeaders; + +/** + * Disk FileUpload implementation that stores file into real files + */ +public class DiskFileUpload extends AbstractDiskHttpData implements FileUpload { + public static String baseDirectory; + + public static boolean deleteOnExitTemporaryFile = true; + + public static String prefix = "FUp_"; + + public static String postfix = ".tmp"; + + private String filename; + + private String contentType; + + private String contentTransferEncoding; + + public DiskFileUpload(String name, String filename, String contentType, + String contentTransferEncoding, Charset charset, long size) { + super(name, charset, size); + setFilename(filename); + setContentType(contentType); + setContentTransferEncoding(contentTransferEncoding); + } + + public HttpDataType getHttpDataType() { + return HttpDataType.FileUpload; + } + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + if (filename == null) { + throw new NullPointerException("filename"); + } + this.filename = filename; + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Attribute)) { + return false; + } + Attribute attribute = (Attribute) o; + return getName().equalsIgnoreCase(attribute.getName()); + } + + public int compareTo(InterfaceHttpData arg0) { + if (!(arg0 instanceof FileUpload)) { + throw new ClassCastException("Cannot compare " + getHttpDataType() + + " with " + arg0.getHttpDataType()); + } + return compareTo((FileUpload) arg0); + } + + public int compareTo(FileUpload o) { + int v; + v = getName().compareToIgnoreCase(o.getName()); + if (v != 0) { + return v; + } + // TODO should we compare size ? + return v; + } + + public void setContentType(String contentType) { + if (contentType == null) { + throw new NullPointerException("contentType"); + } + this.contentType = contentType; + } + + public String getContentType() { + return contentType; + } + + public String getContentTransferEncoding() { + return contentTransferEncoding; + } + + public void setContentTransferEncoding(String contentTransferEncoding) { + this.contentTransferEncoding = contentTransferEncoding; + } + + @Override + public String toString() { + return HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + + HttpPostBodyUtil.FORM_DATA + "; " + HttpPostBodyUtil.NAME + "=\"" + getName() + + "\"; " + HttpPostBodyUtil.FILENAME + "=\"" + filename + "\"\r\n" + + HttpHeaders.Names.CONTENT_TYPE + ": " + contentType + + (charset != null? "; " + HttpHeaders.Values.CHARSET + "=" + charset + "\r\n" : "\r\n") + + HttpHeaders.Names.CONTENT_LENGTH + ": " + length() + "\r\n" + + "Completed: " + isCompleted() + + "\r\nIsInMemory: " + isInMemory() + "\r\nRealFile: " + + file.getAbsolutePath() + " DefaultDeleteAfter: " + + deleteOnExitTemporaryFile; + } + + @Override + protected boolean deleteOnExit() { + return deleteOnExitTemporaryFile; + } + + @Override + protected String getBaseDirectory() { + return baseDirectory; + } + + @Override + protected String getDiskFilename() { + File file = new File(filename); + return file.getName(); + } + + @Override + protected String getPostfix() { + return postfix; + } + + @Override + protected String getPrefix() { + return prefix; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/FileUpload.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/FileUpload.java new file mode 100644 index 0000000000..3eb4596d56 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/FileUpload.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +/** + * FileUpload interface that could be in memory, on temporary file or any other implementations. + * + * Most methods are inspired from java.io.File API. + */ +public interface FileUpload extends HttpData { + /** + * Returns the original filename in the client's filesystem, + * as provided by the browser (or other client software). + * @return the original filename + */ + String getFilename(); + + /** + * Set the original filename + * @param filename + */ + void setFilename(String filename); + + /** + * Set the Content Type passed by the browser if defined + * @param contentType Content Type to set - must be not null + */ + void setContentType(String contentType); + + /** + * Returns the content type passed by the browser or null if not defined. + * @return the content type passed by the browser or null if not defined. + */ + String getContentType(); + + /** + * Set the Content-Transfer-Encoding type from String as 7bit, 8bit or binary + * @param contentTransferEncoding + */ + void setContentTransferEncoding(String contentTransferEncoding); + + /** + * Returns the Content-Transfer-Encoding + * @return the Content-Transfer-Encoding + */ + String getContentTransferEncoding(); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpData.java new file mode 100644 index 0000000000..f3f4b0f90e --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpData.java @@ -0,0 +1,181 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import io.netty.buffer.ByteBuf; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * Extended interface for InterfaceHttpData + */ +public interface HttpData extends InterfaceHttpData { + /** + * Set the content from the ChannelBuffer (erase any previous data) + * + * @param buffer + * must be not null + * @exception IOException + */ + void setContent(ByteBuf buffer) throws IOException; + + /** + * Add the content from the ChannelBuffer + * + * @param buffer + * must be not null except if last is set to False + * @param last + * True of the buffer is the last one + * @exception IOException + */ + void addContent(ByteBuf buffer, boolean last) throws IOException; + + /** + * Set the content from the file (erase any previous data) + * + * @param file + * must be not null + * @exception IOException + */ + void setContent(File file) throws IOException; + + /** + * Set the content from the inputStream (erase any previous data) + * + * @param inputStream + * must be not null + * @exception IOException + */ + void setContent(InputStream inputStream) throws IOException; + + /** + * + * @return True if the InterfaceHttpData is completed (all data are stored) + */ + boolean isCompleted(); + + /** + * Returns the size in byte of the InterfaceHttpData + * + * @return the size of the InterfaceHttpData + */ + long length(); + + /** + * Deletes the underlying storage for a file item, including deleting any + * associated temporary disk file. + */ + void delete(); + + /** + * Returns the contents of the file item as an array of bytes. + * + * @return the contents of the file item as an array of bytes. + * @exception IOException + */ + byte[] get() throws IOException; + + /** + * Returns the content of the file item as a ByteBuf + * + * @return the content of the file item as a ByteBuf + * @throws IOException + */ + ByteBuf getByteBuf() throws IOException; + + /** + * Returns a ChannelBuffer for the content from the current position with at + * most length read bytes, increasing the current position of the Bytes + * read. Once it arrives at the end, it returns an EMPTY_BUFFER and it + * resets the current position to 0. + * + * @param length + * @return a ChannelBuffer for the content from the current position or an + * EMPTY_BUFFER if there is no more data to return + * @throws IOException + */ + ByteBuf getChunk(int length) throws IOException; + + /** + * Returns the contents of the file item as a String, using the default + * character encoding. + * + * @return the contents of the file item as a String, using the default + * character encoding. + * @exception IOException + */ + String getString() throws IOException; + + /** + * Returns the contents of the file item as a String, using the specified + * charset. + * + * @param encoding + * the charset to use + * @return the contents of the file item as a String, using the specified + * charset. + * @exception IOException + */ + String getString(Charset encoding) throws IOException; + + /** + * Set the Charset passed by the browser if defined + * + * @param charset + * Charset to set - must be not null + */ + void setCharset(Charset charset); + + /** + * Returns the Charset passed by the browser or null if not defined. + * + * @return the Charset passed by the browser or null if not defined. + */ + Charset getCharset(); + + /** + * A convenience method to write an uploaded item to disk. If a previous one + * exists, it will be deleted. Once this method is called, if successful, + * the new file will be out of the cleaner of the factory that creates the + * original InterfaceHttpData object. + * + * @param dest + * destination file - must be not null + * @return True if the write is successful + * @exception IOException + */ + boolean renameTo(File dest) throws IOException; + + /** + * Provides a hint as to whether or not the file contents will be read from + * memory. + * + * @return True if the file contents is in memory. + */ + boolean isInMemory(); + + /** + * + * @return the associated File if this data is represented in a file + * @exception IOException + * if this data is not represented by a file + */ + File getFile() throws IOException; + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpDataFactory.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpDataFactory.java new file mode 100644 index 0000000000..6faa5b64e3 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpDataFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.nio.charset.Charset; + +import io.netty.handler.codec.http.HttpRequest; + +/** + * Interface to enable creation of InterfaceHttpData objects + */ +public interface HttpDataFactory { + /** + * + * @param request associated request + * @param name + * @return a new Attribute with no value + * @throws NullPointerException + * @throws IllegalArgumentException + */ + Attribute createAttribute(HttpRequest request, String name); + + /** + * + * @param request associated request + * @param name + * @param value + * @return a new Attribute + * @throws NullPointerException + * @throws IllegalArgumentException + */ + Attribute createAttribute(HttpRequest request, String name, String value); + + /** + * + * @param request associated request + * @param name + * @param filename + * @param contentType + * @param charset + * @param size the size of the Uploaded file + * @return a new FileUpload + */ + FileUpload createFileUpload(HttpRequest request, String name, String filename, + String contentType, String contentTransferEncoding, Charset charset, + long size); + + /** + * Remove the given InterfaceHttpData from clean list (will not delete the file, except if the file + * is still a temporary one as setup at construction) + * @param request associated request + * @param data + */ + void removeHttpDataFromClean(HttpRequest request, InterfaceHttpData data); + + /** + * Remove all InterfaceHttpData from virtual File storage from clean list for the request + * + * @param request associated request + */ + void cleanRequestHttpDatas(HttpRequest request); + + /** + * Remove all InterfaceHttpData from virtual File storage from clean list for all requests + */ + void cleanAllHttpDatas(); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostBodyUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostBodyUtil.java new file mode 100644 index 0000000000..bd81010778 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostBodyUtil.java @@ -0,0 +1,229 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.nio.charset.Charset; + +import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; + +/** + * Shared Static object between HttpMessageDecoder, HttpPostRequestDecoder and HttpPostRequestEncoder + */ +final class HttpPostBodyUtil { + + public static int chunkSize = 8096; + /** + * HTTP content disposition header name. + */ + public static final String CONTENT_DISPOSITION = "Content-Disposition"; + + public static final String NAME = "name"; + + public static final String FILENAME = "filename"; + + /** + * Content-disposition value for form data. + */ + public static final String FORM_DATA = "form-data"; + + /** + * Content-disposition value for file attachment. + */ + public static final String ATTACHMENT = "attachment"; + + /** + * Content-disposition value for file attachment. + */ + public static final String FILE = "file"; + + /** + * HTTP content type body attribute for multiple uploads. + */ + public static final String MULTIPART_MIXED = "multipart/mixed"; + + /** + * Charset for 8BIT + */ + public static final Charset ISO_8859_1 = CharsetUtil.ISO_8859_1; + + /** + * Charset for 7BIT + */ + public static final Charset US_ASCII = CharsetUtil.US_ASCII; + + /** + * Default Content-Type in binary form + */ + public static final String DEFAULT_BINARY_CONTENT_TYPE = "application/octet-stream"; + + /** + * Default Content-Type in Text form + */ + public static final String DEFAULT_TEXT_CONTENT_TYPE = "text/plain"; + + /** + * Allowed mechanism for multipart + * mechanism := "7bit" + / "8bit" + / "binary" + Not allowed: "quoted-printable" + / "base64" + */ + public enum TransferEncodingMechanism { + /** + * Default encoding + */ + BIT7("7bit"), + /** + * Short lines but not in ASCII - no encoding + */ + BIT8("8bit"), + /** + * Could be long text not in ASCII - no encoding + */ + BINARY("binary"); + + public String value; + + TransferEncodingMechanism(String value) { + this.value = value; + } + + TransferEncodingMechanism() { + value = name(); + } + + @Override + public String toString() { + return value; + } + } + + private HttpPostBodyUtil() { + } + + + /** + * Exception when NO Backend Array is found + */ + static class SeekAheadNoBackArrayException extends Exception { + private static final long serialVersionUID = -630418804938699495L; + } + + /** + * This class intends to decrease the CPU in seeking ahead some bytes in + * HttpPostRequestDecoder + */ + static class SeekAheadOptimize { + byte[] bytes; + int readerIndex; + int pos; + int origPos; + int limit; + ByteBuf buffer; + + /** + * @param buffer + */ + SeekAheadOptimize(ByteBuf buffer) throws SeekAheadNoBackArrayException { + if (!buffer.hasArray()) { + throw new SeekAheadNoBackArrayException(); + } + this.buffer = buffer; + bytes = buffer.array(); + readerIndex = buffer.readerIndex(); + origPos = pos = buffer.arrayOffset() + readerIndex; + limit = buffer.arrayOffset() + buffer.writerIndex(); + } + + /** + * + * @param minus this value will be used as (currentPos - minus) to set + * the current readerIndex in the buffer. + */ + void setReadPosition(int minus) { + pos -= minus; + readerIndex = getReadPosition(pos); + buffer.readerIndex(readerIndex); + } + + /** + * + * @param index raw index of the array (pos in general) + * @return the value equivalent of raw index to be used in readerIndex(value) + */ + int getReadPosition(int index) { + return index - origPos + readerIndex; + } + + void clear() { + buffer = null; + bytes = null; + limit = 0; + pos = 0; + readerIndex = 0; + } + } + + /** + * Find the first non whitespace + * @param sb + * @param offset + * @return the rank of the first non whitespace + */ + static int findNonWhitespace(String sb, int offset) { + int result; + for (result = offset; result < sb.length(); result ++) { + if (!Character.isWhitespace(sb.charAt(result))) { + break; + } + } + return result; + } + + /** + * Find the first whitespace + * @param sb + * @param offset + * @return the rank of the first whitespace + */ + static int findWhitespace(String sb, int offset) { + int result; + for (result = offset; result < sb.length(); result ++) { + if (Character.isWhitespace(sb.charAt(result))) { + break; + } + } + return result; + } + + /** + * Find the end of String + * @param sb + * @return the rank of the end of string + */ + static int findEndOfString(String sb) { + int result; + for (result = sb.length(); result > 0; result --) { + if (!Character.isWhitespace(sb.charAt(result - 1))) { + break; + } + } + return result; + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java new file mode 100644 index 0000000000..8d2b282a86 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java @@ -0,0 +1,2124 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpChunk; +import io.netty.handler.codec.http.HttpConstants; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadNoBackArrayException; +import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize; +import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.TransferEncodingMechanism; +import static io.netty.buffer.Unpooled.*; + +/** + * This decoder will decode Body and can handle POST BODY. + */ +public class HttpPostRequestDecoder { + /** + * Factory used to create InterfaceHttpData + */ + private final HttpDataFactory factory; + + /** + * Request to decode + */ + private final HttpRequest request; + + /** + * Default charset to use + */ + private final Charset charset; + + /** + * Does request have a body to decode + */ + private boolean bodyToDecode; + + /** + * Does the last chunk already received + */ + private boolean isLastChunk; + + /** + * HttpDatas from Body + */ + private final List bodyListHttpData = new ArrayList(); + + /** + * HttpDatas as Map from Body + */ + private final Map> bodyMapHttpData = new TreeMap>( + CaseIgnoringComparator.INSTANCE); + + /** + * The current channelBuffer + */ + private ByteBuf undecodedChunk; + + /** + * Does this request is a Multipart request + */ + private boolean isMultipart; + + /** + * Body HttpDatas current position + */ + private int bodyListHttpDataRank; + + /** + * If multipart, this is the boundary for the flobal multipart + */ + private String multipartDataBoundary; + + /** + * If multipart, there could be internal multiparts (mixed) to the global + * multipart. Only one level is allowed. + */ + private String multipartMixedBoundary; + + /** + * Current status + */ + private MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED; + + /** + * Used in Multipart + */ + private Map currentFieldAttributes; + + /** + * The current FileUpload that is currently in decode process + */ + private FileUpload currentFileUpload; + + /** + * The current Attribute that is currently in decode process + */ + private Attribute currentAttribute; + + /** + * + * @param request + * the request to decode + * @throws NullPointerException + * for request + * @throws IncompatibleDataDecoderException + * if the request has no body to decode + * @throws ErrorDataDecoderException + * if the default charset was wrong when decoding or other + * errors + */ + public HttpPostRequestDecoder(HttpRequest request) throws ErrorDataDecoderException, + IncompatibleDataDecoderException { + this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET); + } + + /** + * + * @param factory + * the factory used to create InterfaceHttpData + * @param request + * the request to decode + * @throws NullPointerException + * for request or factory + * @throws IncompatibleDataDecoderException + * if the request has no body to decode + * @throws ErrorDataDecoderException + * if the default charset was wrong when decoding or other + * errors + */ + public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request) throws ErrorDataDecoderException, + IncompatibleDataDecoderException { + this(factory, request, HttpConstants.DEFAULT_CHARSET); + } + + /** + * + * @param factory + * the factory used to create InterfaceHttpData + * @param request + * the request to decode + * @param charset + * the charset to use as default + * @throws NullPointerException + * for request or charset or factory + * @throws IncompatibleDataDecoderException + * if the request has no body to decode + * @throws ErrorDataDecoderException + * if the default charset was wrong when decoding or other + * errors + */ + public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) + throws ErrorDataDecoderException, IncompatibleDataDecoderException { + if (factory == null) { + throw new NullPointerException("factory"); + } + if (request == null) { + throw new NullPointerException("request"); + } + if (charset == null) { + throw new NullPointerException("charset"); + } + this.request = request; + HttpMethod method = request.getMethod(); + if (method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT) || method.equals(HttpMethod.PATCH)) { + bodyToDecode = true; + } + this.charset = charset; + this.factory = factory; + // Fill default values + if (this.request.containsHeader(HttpHeaders.Names.CONTENT_TYPE)) { + checkMultipart(this.request.getHeader(HttpHeaders.Names.CONTENT_TYPE)); + } else { + isMultipart = false; + } + if (!bodyToDecode) { + throw new IncompatibleDataDecoderException("No Body to decode"); + } + if (!this.request.getTransferEncoding().isMultiple()) { + undecodedChunk = this.request.getContent(); + isLastChunk = true; + parseBody(); + } + } + + /** + * states follow NOTSTARTED PREAMBLE ( (HEADERDELIMITER DISPOSITION (FIELD | + * FILEUPLOAD))* (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE (MIXEDDELIMITER + * MIXEDDISPOSITION MIXEDFILEUPLOAD)+ MIXEDCLOSEDELIMITER)* CLOSEDELIMITER)+ + * EPILOGUE + * + * First status is: NOSTARTED + * + * Content-type: multipart/form-data, boundary=AaB03x => PREAMBLE in Header + * + * --AaB03x => HEADERDELIMITER content-disposition: form-data; name="field1" + * => DISPOSITION + * + * Joe Blow => FIELD --AaB03x => HEADERDELIMITER content-disposition: + * form-data; name="pics" => DISPOSITION Content-type: multipart/mixed, + * boundary=BbC04y + * + * --BbC04y => MIXEDDELIMITER Content-disposition: attachment; + * filename="file1.txt" => MIXEDDISPOSITION Content-Type: text/plain + * + * ... contents of file1.txt ... => MIXEDFILEUPLOAD --BbC04y => + * MIXEDDELIMITER Content-disposition: file; filename="file2.gif" => + * MIXEDDISPOSITION Content-type: image/gif Content-Transfer-Encoding: + * binary + * + * ...contents of file2.gif... => MIXEDFILEUPLOAD --BbC04y-- => + * MIXEDCLOSEDELIMITER --AaB03x-- => CLOSEDELIMITER + * + * Once CLOSEDELIMITER is found, last status is EPILOGUE + */ + private enum MultiPartStatus { + NOTSTARTED, PREAMBLE, HEADERDELIMITER, DISPOSITION, FIELD, FILEUPLOAD, MIXEDPREAMBLE, MIXEDDELIMITER, + MIXEDDISPOSITION, MIXEDFILEUPLOAD, MIXEDCLOSEDELIMITER, CLOSEDELIMITER, PREEPILOGUE, EPILOGUE + } + + /** + * Check from the request ContentType if this request is a Multipart + * request. + * + * @param contentType + * @throws ErrorDataDecoderException + */ + private void checkMultipart(String contentType) throws ErrorDataDecoderException { + // Check if Post using "multipart/form-data; boundary=--89421926422648" + String[] headerContentType = splitHeaderContentType(contentType); + if (headerContentType[0].toLowerCase().startsWith(HttpHeaders.Values.MULTIPART_FORM_DATA) + && headerContentType[1].toLowerCase().startsWith(HttpHeaders.Values.BOUNDARY)) { + String[] boundary = headerContentType[1].split("="); + if (boundary.length != 2) { + throw new ErrorDataDecoderException("Needs a boundary value"); + } + multipartDataBoundary = "--" + boundary[1]; + isMultipart = true; + currentStatus = MultiPartStatus.HEADERDELIMITER; + } else { + isMultipart = false; + } + } + + /** + * True if this request is a Multipart request + * + * @return True if this request is a Multipart request + */ + public boolean isMultipart() { + return isMultipart; + } + + /** + * This method returns a List of all HttpDatas from body.
+ * + * If chunked, all chunks must have been offered using offer() method. If + * not, NotEnoughDataDecoderException will be raised. + * + * @return the list of HttpDatas from Body part for POST method + * @throws NotEnoughDataDecoderException + * Need more chunks + */ + public List getBodyHttpDatas() throws NotEnoughDataDecoderException { + if (!isLastChunk) { + throw new NotEnoughDataDecoderException(); + } + return bodyListHttpData; + } + + /** + * This method returns a List of all HttpDatas with the given name from + * body.
+ * + * If chunked, all chunks must have been offered using offer() method. If + * not, NotEnoughDataDecoderException will be raised. + * + * @param name + * @return All Body HttpDatas with the given name (ignore case) + * @throws NotEnoughDataDecoderException + * need more chunks + */ + public List getBodyHttpDatas(String name) throws NotEnoughDataDecoderException { + if (!isLastChunk) { + throw new NotEnoughDataDecoderException(); + } + return bodyMapHttpData.get(name); + } + + /** + * This method returns the first InterfaceHttpData with the given name from + * body.
+ * + * If chunked, all chunks must have been offered using offer() method. If + * not, NotEnoughDataDecoderException will be raised. + * + * @param name + * @return The first Body InterfaceHttpData with the given name (ignore + * case) + * @throws NotEnoughDataDecoderException + * need more chunks + */ + public InterfaceHttpData getBodyHttpData(String name) throws NotEnoughDataDecoderException { + if (!isLastChunk) { + throw new NotEnoughDataDecoderException(); + } + List list = bodyMapHttpData.get(name); + if (list != null) { + return list.get(0); + } + return null; + } + + /** + * Initialized the internals from a new chunk + * + * @param chunk + * the new received chunk + * @throws ErrorDataDecoderException + * if there is a problem with the charset decoding or other + * errors + */ + public void offer(HttpChunk chunk) throws ErrorDataDecoderException { + ByteBuf chunked = chunk.getContent(); + if (undecodedChunk == null) { + undecodedChunk = chunked; + } else { + // undecodedChunk = ByteBufs.wrappedBuffer(undecodedChunk, + // chunk.getContent()); + // less memory usage + undecodedChunk = wrappedBuffer(undecodedChunk, chunked); + } + if (chunk.isLast()) { + isLastChunk = true; + } + parseBody(); + } + + /** + * True if at current status, there is an available decoded + * InterfaceHttpData from the Body. + * + * This method works for chunked and not chunked request. + * + * @return True if at current status, there is a decoded InterfaceHttpData + * @throws EndOfDataDecoderException + * No more data will be available + */ + public boolean hasNext() throws EndOfDataDecoderException { + if (currentStatus == MultiPartStatus.EPILOGUE) { + // OK except if end of list + if (bodyListHttpDataRank >= bodyListHttpData.size()) { + throw new EndOfDataDecoderException(); + } + } + return bodyListHttpData.size() > 0 && bodyListHttpDataRank < bodyListHttpData.size(); + } + + /** + * Returns the next available InterfaceHttpData or null if, at the time it + * is called, there is no more available InterfaceHttpData. A subsequent + * call to offer(httpChunk) could enable more data. + * + * @return the next available InterfaceHttpData or null if none + * @throws EndOfDataDecoderException + * No more data will be available + */ + public InterfaceHttpData next() throws EndOfDataDecoderException { + if (hasNext()) { + return bodyListHttpData.get(bodyListHttpDataRank++); + } + return null; + } + + /** + * This method will parse as much as possible data and fill the list and map + * + * @throws ErrorDataDecoderException + * if there is a problem with the charset decoding or other + * errors + */ + private void parseBody() throws ErrorDataDecoderException { + if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) { + if (isLastChunk) { + currentStatus = MultiPartStatus.EPILOGUE; + } + return; + } + if (isMultipart) { + parseBodyMultipart(); + } else { + parseBodyAttributes(); + } + } + + /** + * Utility function to add a new decoded data + * + * @param data + */ + private void addHttpData(InterfaceHttpData data) { + if (data == null) { + return; + } + List datas = bodyMapHttpData.get(data.getName()); + if (datas == null) { + datas = new ArrayList(1); + bodyMapHttpData.put(data.getName(), datas); + } + datas.add(data); + bodyListHttpData.add(data); + } + + /** + * This method fill the map and list with as much Attribute as possible from + * Body in not Multipart mode. + * + * @throws ErrorDataDecoderException + * if there is a problem with the charset decoding or other + * errors + */ + private void parseBodyAttributesStandard() throws ErrorDataDecoderException { + int firstpos = undecodedChunk.readerIndex(); + int currentpos = firstpos; + int equalpos = firstpos; + int ampersandpos = firstpos; + if (currentStatus == MultiPartStatus.NOTSTARTED) { + currentStatus = MultiPartStatus.DISPOSITION; + } + boolean contRead = true; + try { + while (undecodedChunk.readable() && contRead) { + char read = (char) undecodedChunk.readUnsignedByte(); + currentpos++; + switch (currentStatus) { + case DISPOSITION:// search '=' + if (read == '=') { + currentStatus = MultiPartStatus.FIELD; + equalpos = currentpos - 1; + String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset), + charset); + currentAttribute = factory.createAttribute(request, key); + firstpos = currentpos; + } else if (read == '&') { // special empty FIELD + currentStatus = MultiPartStatus.DISPOSITION; + ampersandpos = currentpos - 1; + String key = decodeAttribute( + undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset); + currentAttribute = factory.createAttribute(request, key); + currentAttribute.setValue(""); // empty + addHttpData(currentAttribute); + currentAttribute = null; + firstpos = currentpos; + contRead = true; + } + break; + case FIELD:// search '&' or end of line + if (read == '&') { + currentStatus = MultiPartStatus.DISPOSITION; + ampersandpos = currentpos - 1; + setFinalBuffer(undecodedChunk.slice(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = true; + } else if (read == HttpConstants.CR) { + if (undecodedChunk.readable()) { + read = (char) undecodedChunk.readUnsignedByte(); + currentpos++; + if (read == HttpConstants.LF) { + currentStatus = MultiPartStatus.PREEPILOGUE; + ampersandpos = currentpos - 2; + setFinalBuffer(undecodedChunk.slice(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = false; + } else { + // Error + contRead = false; + throw new ErrorDataDecoderException("Bad end of line"); + } + } else { + currentpos--; + } + } else if (read == HttpConstants.LF) { + currentStatus = MultiPartStatus.PREEPILOGUE; + ampersandpos = currentpos - 1; + setFinalBuffer(undecodedChunk.slice(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = false; + } + break; + default: + // just stop + contRead = false; + } + } + if (isLastChunk && currentAttribute != null) { + // special case + ampersandpos = currentpos; + if (ampersandpos > firstpos) { + setFinalBuffer(undecodedChunk.slice(firstpos, ampersandpos - firstpos)); + } else if (!currentAttribute.isCompleted()) { + setFinalBuffer(EMPTY_BUFFER); + } + firstpos = currentpos; + currentStatus = MultiPartStatus.EPILOGUE; + return; + } + if (contRead && currentAttribute != null) { + // reset index except if to continue in case of FIELD status + if (currentStatus == MultiPartStatus.FIELD) { + currentAttribute.addContent(undecodedChunk.slice(firstpos, currentpos - firstpos), false); + firstpos = currentpos; + } + undecodedChunk.readerIndex(firstpos); + } else { + // end of line so keep index + } + } catch (ErrorDataDecoderException e) { + // error while decoding + undecodedChunk.readerIndex(firstpos); + throw e; + } catch (IOException e) { + // error while decoding + undecodedChunk.readerIndex(firstpos); + throw new ErrorDataDecoderException(e); + } + } + + /** + * This method fill the map and list with as much Attribute as possible from + * Body in not Multipart mode. + * + * @throws ErrorDataDecoderException + * if there is a problem with the charset decoding or other + * errors + */ + private void parseBodyAttributes() throws ErrorDataDecoderException { + SeekAheadOptimize sao = null; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e1) { + parseBodyAttributesStandard(); + return; + } + int firstpos = undecodedChunk.readerIndex(); + int currentpos = firstpos; + int equalpos = firstpos; + int ampersandpos = firstpos; + if (currentStatus == MultiPartStatus.NOTSTARTED) { + currentStatus = MultiPartStatus.DISPOSITION; + } + boolean contRead = true; + try { + loop: while (sao.pos < sao.limit) { + char read = (char) (sao.bytes[sao.pos++] & 0xFF); + currentpos++; + switch (currentStatus) { + case DISPOSITION:// search '=' + if (read == '=') { + currentStatus = MultiPartStatus.FIELD; + equalpos = currentpos - 1; + String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset), + charset); + currentAttribute = factory.createAttribute(request, key); + firstpos = currentpos; + } else if (read == '&') { // special empty FIELD + currentStatus = MultiPartStatus.DISPOSITION; + ampersandpos = currentpos - 1; + String key = decodeAttribute( + undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset); + currentAttribute = factory.createAttribute(request, key); + currentAttribute.setValue(""); // empty + addHttpData(currentAttribute); + currentAttribute = null; + firstpos = currentpos; + contRead = true; + } + break; + case FIELD:// search '&' or end of line + if (read == '&') { + currentStatus = MultiPartStatus.DISPOSITION; + ampersandpos = currentpos - 1; + setFinalBuffer(undecodedChunk.slice(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = true; + } else if (read == HttpConstants.CR) { + if (sao.pos < sao.limit) { + read = (char) (sao.bytes[sao.pos++] & 0xFF); + currentpos++; + if (read == HttpConstants.LF) { + currentStatus = MultiPartStatus.PREEPILOGUE; + ampersandpos = currentpos - 2; + sao.setReadPosition(0); + setFinalBuffer(undecodedChunk.slice(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = false; + break loop; + } else { + // Error + sao.setReadPosition(0); + contRead = false; + throw new ErrorDataDecoderException("Bad end of line"); + } + } else { + if (sao.limit > 0) { + currentpos--; + } + } + } else if (read == HttpConstants.LF) { + currentStatus = MultiPartStatus.PREEPILOGUE; + ampersandpos = currentpos - 1; + sao.setReadPosition(0); + setFinalBuffer(undecodedChunk.slice(firstpos, ampersandpos - firstpos)); + firstpos = currentpos; + contRead = false; + break loop; + } + break; + default: + // just stop + sao.setReadPosition(0); + contRead = false; + break loop; + } + } + if (isLastChunk && currentAttribute != null) { + // special case + ampersandpos = currentpos; + if (ampersandpos > firstpos) { + setFinalBuffer(undecodedChunk.slice(firstpos, ampersandpos - firstpos)); + } else if (!currentAttribute.isCompleted()) { + setFinalBuffer(EMPTY_BUFFER); + } + firstpos = currentpos; + currentStatus = MultiPartStatus.EPILOGUE; + return; + } + if (contRead && currentAttribute != null) { + // reset index except if to continue in case of FIELD status + if (currentStatus == MultiPartStatus.FIELD) { + currentAttribute.addContent(undecodedChunk.slice(firstpos, currentpos - firstpos), false); + firstpos = currentpos; + } + undecodedChunk.readerIndex(firstpos); + } else { + // end of line so keep index + } + } catch (ErrorDataDecoderException e) { + // error while decoding + undecodedChunk.readerIndex(firstpos); + throw e; + } catch (IOException e) { + // error while decoding + undecodedChunk.readerIndex(firstpos); + throw new ErrorDataDecoderException(e); + } + } + + private void setFinalBuffer(ByteBuf buffer) throws ErrorDataDecoderException, IOException { + currentAttribute.addContent(buffer, true); + String value = decodeAttribute(currentAttribute.getByteBuf().toString(charset), charset); + currentAttribute.setValue(value); + addHttpData(currentAttribute); + currentAttribute = null; + } + + /** + * Decode component + * + * @param s + * @param charset + * @return the decoded component + * @throws ErrorDataDecoderException + */ + private static String decodeAttribute(String s, Charset charset) throws ErrorDataDecoderException { + if (s == null) { + return ""; + } + try { + return URLDecoder.decode(s, charset.name()); + } catch (UnsupportedEncodingException e) { + throw new ErrorDataDecoderException(charset.toString(), e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException("Bad string: '" + s + "'", e); + } + } + + /** + * Parse the Body for multipart + * + * @throws ErrorDataDecoderException + * if there is a problem with the charset decoding or other + * errors + */ + private void parseBodyMultipart() throws ErrorDataDecoderException { + if (undecodedChunk == null || undecodedChunk.readableBytes() == 0) { + // nothing to decode + return; + } + InterfaceHttpData data = decodeMultipart(currentStatus); + while (data != null) { + addHttpData(data); + if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) { + break; + } + data = decodeMultipart(currentStatus); + } + } + + /** + * Decode a multipart request by pieces
+ *
+ * NOTSTARTED PREAMBLE (
+ * (HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))*
+ * (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE
+ * (MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+
+ * MIXEDCLOSEDELIMITER)*
+ * CLOSEDELIMITER)+ EPILOGUE
+ * + * Inspired from HttpMessageDecoder + * + * @param state + * @return the next decoded InterfaceHttpData or null if none until now. + * @throws ErrorDataDecoderException + * if an error occurs + */ + private InterfaceHttpData decodeMultipart(MultiPartStatus state) throws ErrorDataDecoderException { + switch (state) { + case NOTSTARTED: + throw new ErrorDataDecoderException("Should not be called with the current status"); + case PREAMBLE: + // Content-type: multipart/form-data, boundary=AaB03x + throw new ErrorDataDecoderException("Should not be called with the current status"); + case HEADERDELIMITER: { + // --AaB03x or --AaB03x-- + return findMultipartDelimiter(multipartDataBoundary, MultiPartStatus.DISPOSITION, + MultiPartStatus.PREEPILOGUE); + } + case DISPOSITION: { + // content-disposition: form-data; name="field1" + // content-disposition: form-data; name="pics"; filename="file1.txt" + // and other immediate values like + // Content-type: image/gif + // Content-Type: text/plain + // Content-Type: text/plain; charset=ISO-8859-1 + // Content-Transfer-Encoding: binary + // The following line implies a change of mode (mixed mode) + // Content-type: multipart/mixed, boundary=BbC04y + return findMultipartDisposition(); + } + case FIELD: { + // Now get value according to Content-Type and Charset + Charset localCharset = null; + Attribute charsetAttribute = currentFieldAttributes.get(HttpHeaders.Values.CHARSET); + if (charsetAttribute != null) { + try { + localCharset = Charset.forName(charsetAttribute.getValue()); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } + Attribute nameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.NAME); + if (currentAttribute == null) { + try { + currentAttribute = factory.createAttribute(request, nameAttribute.getValue()); + } catch (NullPointerException e) { + throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException(e); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + if (localCharset != null) { + currentAttribute.setCharset(localCharset); + } + } + // load data + try { + loadFieldMultipart(multipartDataBoundary); + } catch (NotEnoughDataDecoderException e) { + return null; + } + Attribute finalAttribute = currentAttribute; + currentAttribute = null; + currentFieldAttributes = null; + // ready to load the next one + currentStatus = MultiPartStatus.HEADERDELIMITER; + return finalAttribute; + } + case FILEUPLOAD: { + // eventually restart from existing FileUpload + return getFileUpload(multipartDataBoundary); + } + case MIXEDDELIMITER: { + // --AaB03x or --AaB03x-- + // Note that currentFieldAttributes exists + return findMultipartDelimiter(multipartMixedBoundary, MultiPartStatus.MIXEDDISPOSITION, + MultiPartStatus.HEADERDELIMITER); + } + case MIXEDDISPOSITION: { + return findMultipartDisposition(); + } + case MIXEDFILEUPLOAD: { + // eventually restart from existing FileUpload + return getFileUpload(multipartMixedBoundary); + } + case PREEPILOGUE: + return null; + case EPILOGUE: + return null; + default: + throw new ErrorDataDecoderException("Shouldn't reach here."); + } + } + + /** + * Skip control Characters + * + * @throws NotEnoughDataDecoderException + */ + void skipControlCharacters() throws NotEnoughDataDecoderException { + SeekAheadOptimize sao = null; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e) { + try { + skipControlCharactersStandard(); + } catch (IndexOutOfBoundsException e1) { + throw new NotEnoughDataDecoderException(e1); + } + return; + } + + while (sao.pos < sao.limit) { + char c = (char) (sao.bytes[sao.pos++] & 0xFF); + if (!Character.isISOControl(c) && !Character.isWhitespace(c)) { + sao.setReadPosition(1); + return; + } + } + throw new NotEnoughDataDecoderException("Access out of bounds"); + } + + void skipControlCharactersStandard() { + for (;;) { + char c = (char) undecodedChunk.readUnsignedByte(); + if (!Character.isISOControl(c) && !Character.isWhitespace(c)) { + undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1); + break; + } + } + } + + /** + * Find the next Multipart Delimiter + * + * @param delimiter + * delimiter to find + * @param dispositionStatus + * the next status if the delimiter is a start + * @param closeDelimiterStatus + * the next status if the delimiter is a close delimiter + * @return the next InterfaceHttpData if any + * @throws ErrorDataDecoderException + */ + private InterfaceHttpData findMultipartDelimiter(String delimiter, MultiPartStatus dispositionStatus, + MultiPartStatus closeDelimiterStatus) throws ErrorDataDecoderException { + // --AaB03x or --AaB03x-- + int readerIndex = undecodedChunk.readerIndex(); + try { + skipControlCharacters(); + } catch (NotEnoughDataDecoderException e1) { + undecodedChunk.readerIndex(readerIndex); + return null; + } + skipOneLine(); + String newline; + try { + newline = readDelimiter(delimiter); + } catch (NotEnoughDataDecoderException e) { + undecodedChunk.readerIndex(readerIndex); + return null; + } + if (newline.equals(delimiter)) { + currentStatus = dispositionStatus; + return decodeMultipart(dispositionStatus); + } else if (newline.equals(delimiter + "--")) { + // CLOSEDELIMITER or MIXED CLOSEDELIMITER found + currentStatus = closeDelimiterStatus; + if (currentStatus == MultiPartStatus.HEADERDELIMITER) { + // MIXEDCLOSEDELIMITER + // end of the Mixed part + currentFieldAttributes = null; + return decodeMultipart(MultiPartStatus.HEADERDELIMITER); + } + return null; + } + undecodedChunk.readerIndex(readerIndex); + throw new ErrorDataDecoderException("No Multipart delimiter found"); + } + + /** + * Find the next Disposition + * + * @return the next InterfaceHttpData if any + * @throws ErrorDataDecoderException + */ + private InterfaceHttpData findMultipartDisposition() throws ErrorDataDecoderException { + int readerIndex = undecodedChunk.readerIndex(); + if (currentStatus == MultiPartStatus.DISPOSITION) { + currentFieldAttributes = new TreeMap(CaseIgnoringComparator.INSTANCE); + } + // read many lines until empty line with newline found! Store all data + while (!skipOneLine()) { + String newline; + try { + skipControlCharacters(); + newline = readLine(); + } catch (NotEnoughDataDecoderException e) { + undecodedChunk.readerIndex(readerIndex); + return null; + } + String[] contents = splitMultipartHeader(newline); + if (contents[0].equalsIgnoreCase(HttpPostBodyUtil.CONTENT_DISPOSITION)) { + boolean checkSecondArg = false; + if (currentStatus == MultiPartStatus.DISPOSITION) { + checkSecondArg = contents[1].equalsIgnoreCase(HttpPostBodyUtil.FORM_DATA); + } else { + checkSecondArg = contents[1].equalsIgnoreCase(HttpPostBodyUtil.ATTACHMENT) + || contents[1].equalsIgnoreCase(HttpPostBodyUtil.FILE); + } + if (checkSecondArg) { + // read next values and store them in the map as Attribute + for (int i = 2; i < contents.length; i++) { + String[] values = contents[i].split("="); + Attribute attribute; + try { + attribute = factory.createAttribute(request, values[0].trim(), + decodeAttribute(cleanString(values[1]), charset)); + } catch (NullPointerException e) { + throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException(e); + } + currentFieldAttributes.put(attribute.getName(), attribute); + } + } + } else if (contents[0].equalsIgnoreCase(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING)) { + Attribute attribute; + try { + attribute = factory.createAttribute(request, HttpHeaders.Names.CONTENT_TRANSFER_ENCODING, + cleanString(contents[1])); + } catch (NullPointerException e) { + throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException(e); + } + currentFieldAttributes.put(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING, attribute); + } else if (contents[0].equalsIgnoreCase(HttpHeaders.Names.CONTENT_LENGTH)) { + Attribute attribute; + try { + attribute = factory.createAttribute(request, HttpHeaders.Names.CONTENT_LENGTH, + cleanString(contents[1])); + } catch (NullPointerException e) { + throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException(e); + } + currentFieldAttributes.put(HttpHeaders.Names.CONTENT_LENGTH, attribute); + } else if (contents[0].equalsIgnoreCase(HttpHeaders.Names.CONTENT_TYPE)) { + // Take care of possible "multipart/mixed" + if (contents[1].equalsIgnoreCase(HttpPostBodyUtil.MULTIPART_MIXED)) { + if (currentStatus == MultiPartStatus.DISPOSITION) { + String[] values = contents[2].split("="); + multipartMixedBoundary = "--" + values[1]; + currentStatus = MultiPartStatus.MIXEDDELIMITER; + return decodeMultipart(MultiPartStatus.MIXEDDELIMITER); + } else { + throw new ErrorDataDecoderException("Mixed Multipart found in a previous Mixed Multipart"); + } + } else { + for (int i = 1; i < contents.length; i++) { + if (contents[i].toLowerCase().startsWith(HttpHeaders.Values.CHARSET)) { + String[] values = contents[i].split("="); + Attribute attribute; + try { + attribute = factory.createAttribute(request, HttpHeaders.Values.CHARSET, + cleanString(values[1])); + } catch (NullPointerException e) { + throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException(e); + } + currentFieldAttributes.put(HttpHeaders.Values.CHARSET, attribute); + } else { + Attribute attribute; + try { + attribute = factory.createAttribute(request, contents[0].trim(), + decodeAttribute(cleanString(contents[i]), charset)); + } catch (NullPointerException e) { + throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException(e); + } + currentFieldAttributes.put(attribute.getName(), attribute); + } + } + } + } else { + throw new ErrorDataDecoderException("Unknown Params: " + newline); + } + } + // Is it a FileUpload + Attribute filenameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.FILENAME); + if (currentStatus == MultiPartStatus.DISPOSITION) { + if (filenameAttribute != null) { + // FileUpload + currentStatus = MultiPartStatus.FILEUPLOAD; + // do not change the buffer position + return decodeMultipart(MultiPartStatus.FILEUPLOAD); + } else { + // Field + currentStatus = MultiPartStatus.FIELD; + // do not change the buffer position + return decodeMultipart(MultiPartStatus.FIELD); + } + } else { + if (filenameAttribute != null) { + // FileUpload + currentStatus = MultiPartStatus.MIXEDFILEUPLOAD; + // do not change the buffer position + return decodeMultipart(MultiPartStatus.MIXEDFILEUPLOAD); + } else { + // Field is not supported in MIXED mode + throw new ErrorDataDecoderException("Filename not found"); + } + } + } + + /** + * Get the FileUpload (new one or current one) + * + * @param delimiter + * the delimiter to use + * @return the InterfaceHttpData if any + * @throws ErrorDataDecoderException + */ + private InterfaceHttpData getFileUpload(String delimiter) throws ErrorDataDecoderException { + // eventually restart from existing FileUpload + // Now get value according to Content-Type and Charset + Attribute encoding = currentFieldAttributes.get(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING); + Charset localCharset = charset; + // Default + TransferEncodingMechanism mechanism = TransferEncodingMechanism.BIT7; + if (encoding != null) { + String code; + try { + code = encoding.getValue().toLowerCase(); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT7.value)) { + localCharset = HttpPostBodyUtil.US_ASCII; + } else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT8.value)) { + localCharset = HttpPostBodyUtil.ISO_8859_1; + mechanism = TransferEncodingMechanism.BIT8; + } else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value)) { + // no real charset, so let the default + mechanism = TransferEncodingMechanism.BINARY; + } else { + throw new ErrorDataDecoderException("TransferEncoding Unknown: " + code); + } + } + Attribute charsetAttribute = currentFieldAttributes.get(HttpHeaders.Values.CHARSET); + if (charsetAttribute != null) { + try { + localCharset = Charset.forName(charsetAttribute.getValue()); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } + if (currentFileUpload == null) { + Attribute filenameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.FILENAME); + Attribute nameAttribute = currentFieldAttributes.get(HttpPostBodyUtil.NAME); + Attribute contentTypeAttribute = currentFieldAttributes.get(HttpHeaders.Names.CONTENT_TYPE); + if (contentTypeAttribute == null) { + throw new ErrorDataDecoderException("Content-Type is absent but required"); + } + Attribute lengthAttribute = currentFieldAttributes.get(HttpHeaders.Names.CONTENT_LENGTH); + long size; + try { + size = lengthAttribute != null ? Long.parseLong(lengthAttribute.getValue()) : 0L; + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } catch (NumberFormatException e) { + size = 0; + } + try { + currentFileUpload = factory.createFileUpload(request, nameAttribute.getValue(), + filenameAttribute.getValue(), contentTypeAttribute.getValue(), mechanism.value, localCharset, + size); + } catch (NullPointerException e) { + throw new ErrorDataDecoderException(e); + } catch (IllegalArgumentException e) { + throw new ErrorDataDecoderException(e); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } + // load data as much as possible + try { + readFileUploadByteMultipart(delimiter); + } catch (NotEnoughDataDecoderException e) { + // do not change the buffer position + // since some can be already saved into FileUpload + // So do not change the currentStatus + return null; + } + if (currentFileUpload.isCompleted()) { + // ready to load the next one + if (currentStatus == MultiPartStatus.FILEUPLOAD) { + currentStatus = MultiPartStatus.HEADERDELIMITER; + currentFieldAttributes = null; + } else { + currentStatus = MultiPartStatus.MIXEDDELIMITER; + cleanMixedAttributes(); + } + FileUpload fileUpload = currentFileUpload; + currentFileUpload = null; + return fileUpload; + } + // do not change the buffer position + // since some can be already saved into FileUpload + // So do not change the currentStatus + return null; + } + + /** + * Clean all HttpDatas (on Disk) for the current request. + */ + public void cleanFiles() { + factory.cleanRequestHttpDatas(request); + } + + /** + * Remove the given FileUpload from the list of FileUploads to clean + */ + public void removeHttpDataFromClean(InterfaceHttpData data) { + factory.removeHttpDataFromClean(request, data); + } + + /** + * Remove all Attributes that should be cleaned between two FileUpload in + * Mixed mode + */ + private void cleanMixedAttributes() { + currentFieldAttributes.remove(HttpHeaders.Values.CHARSET); + currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_LENGTH); + currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING); + currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_TYPE); + currentFieldAttributes.remove(HttpPostBodyUtil.FILENAME); + } + + /** + * Read one line up to the CRLF or LF + * + * @return the String from one line + * @throws NotEnoughDataDecoderException + * Need more chunks and reset the readerInder to the previous + * value + */ + private String readLineStandard() throws NotEnoughDataDecoderException { + int readerIndex = undecodedChunk.readerIndex(); + try { + StringBuilder sb = new StringBuilder(64); + while (undecodedChunk.readable()) { + byte nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.CR) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + return sb.toString(); + } + } else if (nextByte == HttpConstants.LF) { + return sb.toString(); + } else { + sb.append((char) nextByte); + } + } + } catch (IndexOutOfBoundsException e) { + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(e); + } + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(); + } + + /** + * Read one line up to the CRLF or LF + * + * @return the String from one line + * @throws NotEnoughDataDecoderException + * Need more chunks and reset the readerInder to the previous + * value + */ + private String readLine() throws NotEnoughDataDecoderException { + SeekAheadOptimize sao = null; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e1) { + return readLineStandard(); + } + int readerIndex = undecodedChunk.readerIndex(); + try { + StringBuilder sb = new StringBuilder(64); + while (sao.pos < sao.limit) { + byte nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.CR) { + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.LF) { + sao.setReadPosition(0); + return sb.toString(); + } + } else { + sb.append((char) nextByte); + } + } else if (nextByte == HttpConstants.LF) { + sao.setReadPosition(0); + return sb.toString(); + } else { + sb.append((char) nextByte); + } + } + } catch (IndexOutOfBoundsException e) { + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(e); + } + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(); + } + + /** + * Read one line up to --delimiter or --delimiter-- and if existing the CRLF + * or LF Read one line up to --delimiter or --delimiter-- and if existing + * the CRLF or LF. Note that CRLF or LF are mandatory for opening delimiter + * (--delimiter) but not for closing delimiter (--delimiter--) since some + * clients does not include CRLF in this case. + * + * @param delimiter + * of the form --string, such that '--' is already included + * @return the String from one line as the delimiter searched (opening or + * closing) + * @throws NotEnoughDataDecoderException + * Need more chunks and reset the readerInder to the previous + * value + */ + private String readDelimiterStandard(String delimiter) throws NotEnoughDataDecoderException { + int readerIndex = undecodedChunk.readerIndex(); + try { + StringBuilder sb = new StringBuilder(64); + int delimiterPos = 0; + int len = delimiter.length(); + while (undecodedChunk.readable() && delimiterPos < len) { + byte nextByte = undecodedChunk.readByte(); + if (nextByte == delimiter.charAt(delimiterPos)) { + delimiterPos++; + sb.append((char) nextByte); + } else { + // delimiter not found so break here ! + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(); + } + } + // Now check if either opening delimiter or closing delimiter + if (undecodedChunk.readable()) { + byte nextByte = undecodedChunk.readByte(); + // first check for opening delimiter + if (nextByte == HttpConstants.CR) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + return sb.toString(); + } else { + // error since CR must be followed by LF + // delimiter not found so break here ! + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(); + } + } else if (nextByte == HttpConstants.LF) { + return sb.toString(); + } else if (nextByte == '-') { + sb.append((char) nextByte); + // second check for closing delimiter + nextByte = undecodedChunk.readByte(); + if (nextByte == '-') { + sb.append((char) nextByte); + // now try to find if CRLF or LF there + if (undecodedChunk.readable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.CR) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + return sb.toString(); + } else { + // error CR without LF + // delimiter not found so break here ! + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(); + } + } else if (nextByte == HttpConstants.LF) { + return sb.toString(); + } else { + // No CRLF but ok however (Adobe Flash uploader) + // minus 1 since we read one char ahead but + // should not + undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1); + return sb.toString(); + } + } + // FIXME what do we do here? + // either considering it is fine, either waiting for + // more data to come? + // lets try considering it is fine... + return sb.toString(); + } + // only one '-' => not enough + // whatever now => error since incomplete + } + } + } catch (IndexOutOfBoundsException e) { + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(e); + } + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(); + } + + /** + * Read one line up to --delimiter or --delimiter-- and if existing the CRLF + * or LF. Note that CRLF or LF are mandatory for opening delimiter + * (--delimiter) but not for closing delimiter (--delimiter--) since some + * clients does not include CRLF in this case. + * + * @param delimiter + * of the form --string, such that '--' is already included + * @return the String from one line as the delimiter searched (opening or + * closing) + * @throws NotEnoughDataDecoderException + * Need more chunks and reset the readerInder to the previous + * value + */ + private String readDelimiter(String delimiter) throws NotEnoughDataDecoderException { + SeekAheadOptimize sao = null; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e1) { + return readDelimiterStandard(delimiter); + } + int readerIndex = undecodedChunk.readerIndex(); + int delimiterPos = 0; + int len = delimiter.length(); + try { + StringBuilder sb = new StringBuilder(64); + // check conformity with delimiter + while (sao.pos < sao.limit && delimiterPos < len) { + byte nextByte = sao.bytes[sao.pos++]; + if (nextByte == delimiter.charAt(delimiterPos)) { + delimiterPos++; + sb.append((char) nextByte); + } else { + // delimiter not found so break here ! + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(); + } + } + // Now check if either opening delimiter or closing delimiter + if (sao.pos < sao.limit) { + byte nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.CR) { + // first check for opening delimiter + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.LF) { + sao.setReadPosition(0); + return sb.toString(); + } + } else { + // error since CR must be followed by LF + // delimiter not found so break here ! + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(); + } + } else if (nextByte == HttpConstants.LF) { + // same first check for opening delimiter where LF used with + // no CR + sao.setReadPosition(0); + return sb.toString(); + } else if (nextByte == '-') { + sb.append((char) nextByte); + // second check for closing delimiter + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == '-') { + sb.append((char) nextByte); + // now try to find if CRLF or LF there + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.CR) { + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.LF) { + sao.setReadPosition(0); + return sb.toString(); + } + } else { + // error CR without LF + // delimiter not found so break here ! + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(); + } + } else if (nextByte == HttpConstants.LF) { + sao.setReadPosition(0); + return sb.toString(); + } else { + // No CRLF but ok however (Adobe Flash + // uploader) + // minus 1 since we read one char ahead but + // should not + sao.setReadPosition(1); + return sb.toString(); + } + } + // FIXME what do we do here? + // either considering it is fine, either waiting for + // more data to come? + // lets try considering it is fine... + sao.setReadPosition(0); + return sb.toString(); + } + // whatever now => error since incomplete + // only one '-' => not enough or whatever not enough + // element + } + } + } + } catch (IndexOutOfBoundsException e) { + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(e); + } + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(); + } + + /** + * Read a FileUpload data as Byte (Binary) and add the bytes directly to the + * FileUpload. If the delimiter is found, the FileUpload is completed. + * + * @param delimiter + * @throws NotEnoughDataDecoderException + * Need more chunks but do not reset the readerInder since some + * values will be already added to the FileOutput + * @throws ErrorDataDecoderException + * write IO error occurs with the FileUpload + */ + private void readFileUploadByteMultipartStandard(String delimiter) throws NotEnoughDataDecoderException, + ErrorDataDecoderException { + int readerIndex = undecodedChunk.readerIndex(); + // found the decoder limit + boolean newLine = true; + int index = 0; + int lastPosition = undecodedChunk.readerIndex(); + boolean found = false; + while (undecodedChunk.readable()) { + byte nextByte = undecodedChunk.readByte(); + if (newLine) { + // Check the delimiter + if (nextByte == delimiter.codePointAt(index)) { + index++; + if (delimiter.length() == index) { + found = true; + break; + } + continue; + } else { + newLine = false; + index = 0; + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (undecodedChunk.readable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 1; + } else { + // save last valid position + lastPosition = undecodedChunk.readerIndex(); + } + } + } else { + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (undecodedChunk.readable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 1; + } else { + // save last valid position + lastPosition = undecodedChunk.readerIndex(); + } + } + } + ByteBuf buffer = undecodedChunk.slice(readerIndex, lastPosition - readerIndex); + if (found) { + // found so lastPosition is correct and final + try { + currentFileUpload.addContent(buffer, true); + // just before the CRLF and delimiter + undecodedChunk.readerIndex(lastPosition); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } else { + // possibly the delimiter is partially found but still the last + // position is OK + try { + currentFileUpload.addContent(buffer, false); + // last valid char (not CR, not LF, not beginning of delimiter) + undecodedChunk.readerIndex(lastPosition); + throw new NotEnoughDataDecoderException(); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } + } + + /** + * Read a FileUpload data as Byte (Binary) and add the bytes directly to the + * FileUpload. If the delimiter is found, the FileUpload is completed. + * + * @param delimiter + * @throws NotEnoughDataDecoderException + * Need more chunks but do not reset the readerInder since some + * values will be already added to the FileOutput + * @throws ErrorDataDecoderException + * write IO error occurs with the FileUpload + */ + private void readFileUploadByteMultipart(String delimiter) throws NotEnoughDataDecoderException, + ErrorDataDecoderException { + SeekAheadOptimize sao = null; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e1) { + readFileUploadByteMultipartStandard(delimiter); + return; + } + int readerIndex = undecodedChunk.readerIndex(); + // found the decoder limit + boolean newLine = true; + int index = 0; + int lastrealpos = sao.pos; + int lastPosition = undecodedChunk.readerIndex(); + boolean found = false; + + while (sao.pos < sao.limit) { + byte nextByte = sao.bytes[sao.pos++]; + if (newLine) { + // Check the delimiter + if (nextByte == delimiter.codePointAt(index)) { + index++; + if (delimiter.length() == index) { + found = true; + break; + } + continue; + } else { + newLine = false; + index = 0; + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 1; + } else { + // save last valid position + lastrealpos = sao.pos; + } + } + } else { + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 1; + } else { + // save last valid position + lastrealpos = sao.pos; + } + } + } + lastPosition = sao.getReadPosition(lastrealpos); + ByteBuf buffer = undecodedChunk.slice(readerIndex, lastPosition - readerIndex); + if (found) { + // found so lastPosition is correct and final + try { + currentFileUpload.addContent(buffer, true); + // just before the CRLF and delimiter + undecodedChunk.readerIndex(lastPosition); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } else { + // possibly the delimiter is partially found but still the last + // position is OK + try { + currentFileUpload.addContent(buffer, false); + // last valid char (not CR, not LF, not beginning of delimiter) + undecodedChunk.readerIndex(lastPosition); + throw new NotEnoughDataDecoderException(); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + } + } + + /** + * Load the field value from a Multipart request + * + * @throws NotEnoughDataDecoderException + * Need more chunks + * @throws ErrorDataDecoderException + */ + private void loadFieldMultipartStandard(String delimiter) throws NotEnoughDataDecoderException, + ErrorDataDecoderException { + int readerIndex = undecodedChunk.readerIndex(); + try { + // found the decoder limit + boolean newLine = true; + int index = 0; + int lastPosition = undecodedChunk.readerIndex(); + boolean found = false; + while (undecodedChunk.readable()) { + byte nextByte = undecodedChunk.readByte(); + if (newLine) { + // Check the delimiter + if (nextByte == delimiter.codePointAt(index)) { + index++; + if (delimiter.length() == index) { + found = true; + break; + } + continue; + } else { + newLine = false; + index = 0; + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (undecodedChunk.readable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 1; + } else { + lastPosition = undecodedChunk.readerIndex(); + } + } + } else { + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (undecodedChunk.readable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 1; + } else { + lastPosition = undecodedChunk.readerIndex(); + } + } + } + if (found) { + // found so lastPosition is correct + // but position is just after the delimiter (either close + // delimiter or simple one) + // so go back of delimiter size + try { + currentAttribute.addContent(undecodedChunk.slice(readerIndex, lastPosition - readerIndex), true); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + undecodedChunk.readerIndex(lastPosition); + } else { + try { + currentAttribute.addContent(undecodedChunk.slice(readerIndex, lastPosition - readerIndex), false); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + undecodedChunk.readerIndex(lastPosition); + throw new NotEnoughDataDecoderException(); + } + } catch (IndexOutOfBoundsException e) { + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(e); + } + } + + /** + * Load the field value from a Multipart request + * + * @throws NotEnoughDataDecoderException + * Need more chunks + * @throws ErrorDataDecoderException + */ + private void loadFieldMultipart(String delimiter) throws NotEnoughDataDecoderException, ErrorDataDecoderException { + SeekAheadOptimize sao = null; + try { + sao = new SeekAheadOptimize(undecodedChunk); + } catch (SeekAheadNoBackArrayException e1) { + loadFieldMultipartStandard(delimiter); + return; + } + int readerIndex = undecodedChunk.readerIndex(); + try { + // found the decoder limit + boolean newLine = true; + int index = 0; + int lastPosition = undecodedChunk.readerIndex(); + int lastrealpos = sao.pos; + boolean found = false; + + while (sao.pos < sao.limit) { + byte nextByte = sao.bytes[sao.pos++]; + if (newLine) { + // Check the delimiter + if (nextByte == delimiter.codePointAt(index)) { + index++; + if (delimiter.length() == index) { + found = true; + break; + } + continue; + } else { + newLine = false; + index = 0; + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 1; + } else { + lastrealpos = sao.pos; + } + } + } else { + // continue until end of line + if (nextByte == HttpConstants.CR) { + if (sao.pos < sao.limit) { + nextByte = sao.bytes[sao.pos++]; + if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 2; + } + } + } else if (nextByte == HttpConstants.LF) { + newLine = true; + index = 0; + lastrealpos = sao.pos - 1; + } else { + lastrealpos = sao.pos; + } + } + } + lastPosition = sao.getReadPosition(lastrealpos); + if (found) { + // found so lastPosition is correct + // but position is just after the delimiter (either close + // delimiter or simple one) + // so go back of delimiter size + try { + currentAttribute.addContent(undecodedChunk.slice(readerIndex, lastPosition - readerIndex), true); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + undecodedChunk.readerIndex(lastPosition); + } else { + try { + currentAttribute.addContent(undecodedChunk.slice(readerIndex, lastPosition - readerIndex), false); + } catch (IOException e) { + throw new ErrorDataDecoderException(e); + } + undecodedChunk.readerIndex(lastPosition); + throw new NotEnoughDataDecoderException(); + } + } catch (IndexOutOfBoundsException e) { + undecodedChunk.readerIndex(readerIndex); + throw new NotEnoughDataDecoderException(e); + } + } + + /** + * Clean the String from any unallowed character + * + * @return the cleaned String + */ + private static String cleanString(String field) { + StringBuilder sb = new StringBuilder(field.length()); + int i = 0; + for (i = 0; i < field.length(); i++) { + char nextChar = field.charAt(i); + if (nextChar == HttpConstants.COLON) { + sb.append(HttpConstants.SP); + } else if (nextChar == HttpConstants.COMMA) { + sb.append(HttpConstants.SP); + } else if (nextChar == HttpConstants.EQUALS) { + sb.append(HttpConstants.SP); + } else if (nextChar == HttpConstants.SEMICOLON) { + sb.append(HttpConstants.SP); + } else if (nextChar == HttpConstants.HT) { + sb.append(HttpConstants.SP); + } else if (nextChar == HttpConstants.DOUBLE_QUOTE) { + // nothing added, just removes it + } else { + sb.append(nextChar); + } + } + return sb.toString().trim(); + } + + /** + * Skip one empty line + * + * @return True if one empty line was skipped + */ + private boolean skipOneLine() { + if (!undecodedChunk.readable()) { + return false; + } + byte nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.CR) { + if (!undecodedChunk.readable()) { + undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1); + return false; + } + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpConstants.LF) { + return true; + } + undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 2); + return false; + } else if (nextByte == HttpConstants.LF) { + return true; + } + undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1); + return false; + } + + /** + * Split the very first line (Content-Type value) in 2 Strings + * + * @param sb + * @return the array of 2 Strings + */ + private static String[] splitHeaderContentType(String sb) { + int size = sb.length(); + int aStart; + int aEnd; + int bStart; + int bEnd; + aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0); + aEnd = HttpPostBodyUtil.findWhitespace(sb, aStart); + if (aEnd >= size) { + return new String[] { sb, "" }; + } + if (sb.charAt(aEnd) == ';') { + aEnd--; + } + bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd); + bEnd = HttpPostBodyUtil.findEndOfString(sb); + return new String[] { sb.substring(aStart, aEnd), sb.substring(bStart, bEnd) }; + } + + /** + * Split one header in Multipart + * + * @param sb + * @return an array of String where rank 0 is the name of the header, + * follows by several values that were separated by ';' or ',' + */ + private static String[] splitMultipartHeader(String sb) { + ArrayList headers = new ArrayList(1); + int nameStart; + int nameEnd; + int colonEnd; + int valueStart; + int valueEnd; + nameStart = HttpPostBodyUtil.findNonWhitespace(sb, 0); + for (nameEnd = nameStart; nameEnd < sb.length(); nameEnd++) { + char ch = sb.charAt(nameEnd); + if (ch == ':' || Character.isWhitespace(ch)) { + break; + } + } + for (colonEnd = nameEnd; colonEnd < sb.length(); colonEnd++) { + if (sb.charAt(colonEnd) == ':') { + colonEnd++; + break; + } + } + valueStart = HttpPostBodyUtil.findNonWhitespace(sb, colonEnd); + valueEnd = HttpPostBodyUtil.findEndOfString(sb); + headers.add(sb.substring(nameStart, nameEnd)); + String svalue = sb.substring(valueStart, valueEnd); + String[] values = null; + if (svalue.indexOf(";") >= 0) { + values = svalue.split(";"); + } else { + values = svalue.split(","); + } + for (String value : values) { + headers.add(value.trim()); + } + String[] array = new String[headers.size()]; + for (int i = 0; i < headers.size(); i++) { + array[i] = headers.get(i); + } + return array; + } + + /** + * Exception when try reading data from request in chunked format, and not + * enough data are available (need more chunks) + */ + public static class NotEnoughDataDecoderException extends Exception { + /** + */ + private static final long serialVersionUID = -7846841864603865638L; + + /** + */ + public NotEnoughDataDecoderException() { + } + + /** + * @param arg0 + */ + public NotEnoughDataDecoderException(String arg0) { + super(arg0); + } + + /** + * @param arg0 + */ + public NotEnoughDataDecoderException(Throwable arg0) { + super(arg0); + } + + /** + * @param arg0 + * @param arg1 + */ + public NotEnoughDataDecoderException(String arg0, Throwable arg1) { + super(arg0, arg1); + } + } + + /** + * Exception when the body is fully decoded, even if there is still data + */ + public static class EndOfDataDecoderException extends Exception { + /** + */ + private static final long serialVersionUID = 1336267941020800769L; + + } + + /** + * Exception when an error occurs while decoding + */ + public static class ErrorDataDecoderException extends Exception { + /** + */ + private static final long serialVersionUID = 5020247425493164465L; + + /** + */ + public ErrorDataDecoderException() { + } + + /** + * @param arg0 + */ + public ErrorDataDecoderException(String arg0) { + super(arg0); + } + + /** + * @param arg0 + */ + public ErrorDataDecoderException(Throwable arg0) { + super(arg0); + } + + /** + * @param arg0 + * @param arg1 + */ + public ErrorDataDecoderException(String arg0, Throwable arg1) { + super(arg0, arg1); + } + } + + /** + * Exception when an unappropriated method was called on a request + */ + public static class IncompatibleDataDecoderException extends Exception { + /** + */ + private static final long serialVersionUID = -953268047926250267L; + + /** + */ + public IncompatibleDataDecoderException() { + } + + /** + * @param arg0 + */ + public IncompatibleDataDecoderException(String arg0) { + super(arg0); + } + + /** + * @param arg0 + */ + public IncompatibleDataDecoderException(Throwable arg0) { + super(arg0); + } + + /** + * @param arg0 + * @param arg1 + */ + public IncompatibleDataDecoderException(String arg0, Throwable arg1) { + super(arg0, arg1); + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java new file mode 100644 index 0000000000..8b87a0f8db --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java @@ -0,0 +1,1021 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.Random; + +import static io.netty.buffer.Unpooled.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.MessageBuf; +import io.netty.handler.codec.http.DefaultHttpChunk; +import io.netty.handler.codec.http.HttpChunk; +import io.netty.handler.codec.http.HttpConstants; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpTransferEncoding; +import io.netty.handler.stream.ChunkedMessageInput; + +/** + * This encoder will help to encode Request for a FORM as POST. + */ +public class HttpPostRequestEncoder implements ChunkedMessageInput { + /** + * Factory used to create InterfaceHttpData + */ + private final HttpDataFactory factory; + + /** + * Request to encode + */ + private final HttpRequest request; + + /** + * Default charset to use + */ + private final Charset charset; + + /** + * Chunked false by default + */ + private boolean isChunked; + + /** + * InterfaceHttpData for Body (without encoding) + */ + private final List bodyListDatas; + /** + * The final Multipart List of InterfaceHttpData including encoding + */ + private final List multipartHttpDatas; + + /** + * Does this request is a Multipart request + */ + private final boolean isMultipart; + + /** + * If multipart, this is the boundary for the flobal multipart + */ + private String multipartDataBoundary; + + /** + * If multipart, there could be internal multiparts (mixed) to the global multipart. Only one level is allowed. + */ + private String multipartMixedBoundary; + /** + * To check if the header has been finalized + */ + private boolean headerFinalized; + + /** + * + * @param request + * the request to encode + * @param multipart + * True if the FORM is a ENCTYPE="multipart/form-data" + * @throws NullPointerException + * for request + * @throws ErrorDataEncoderException + * if the request is not a POST + */ + public HttpPostRequestEncoder(HttpRequest request, boolean multipart) throws ErrorDataEncoderException { + this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, multipart, + HttpConstants.DEFAULT_CHARSET); + } + + /** + * + * @param factory + * the factory used to create InterfaceHttpData + * @param request + * the request to encode + * @param multipart + * True if the FORM is a ENCTYPE="multipart/form-data" + * @throws NullPointerException + * for request and factory + * @throws ErrorDataEncoderException + * if the request is not a POST + */ + public HttpPostRequestEncoder(HttpDataFactory factory, HttpRequest request, boolean multipart) + throws ErrorDataEncoderException { + this(factory, request, multipart, HttpConstants.DEFAULT_CHARSET); + } + + /** + * + * @param factory + * the factory used to create InterfaceHttpData + * @param request + * the request to encode + * @param multipart + * True if the FORM is a ENCTYPE="multipart/form-data" + * @param charset + * the charset to use as default + * @throws NullPointerException + * for request or charset or factory + * @throws ErrorDataEncoderException + * if the request is not a POST + */ + public HttpPostRequestEncoder(HttpDataFactory factory, HttpRequest request, boolean multipart, Charset charset) + throws ErrorDataEncoderException { + if (factory == null) { + throw new NullPointerException("factory"); + } + if (request == null) { + throw new NullPointerException("request"); + } + if (charset == null) { + throw new NullPointerException("charset"); + } + if (request.getMethod() != HttpMethod.POST) { + throw new ErrorDataEncoderException("Cannot create a Encoder if not a POST"); + } + this.request = request; + this.charset = charset; + this.factory = factory; + // Fill default values + bodyListDatas = new ArrayList(); + // default mode + isLastChunk = false; + isLastChunkSent = false; + isMultipart = multipart; + multipartHttpDatas = new ArrayList(); + if (isMultipart) { + initDataMultipart(); + } + } + + /** + * Clean all HttpDatas (on Disk) for the current request. + */ + public void cleanFiles() { + factory.cleanRequestHttpDatas(request); + } + + /** + * Does the last non empty chunk already encoded so that next chunk will be empty (last chunk) + */ + private boolean isLastChunk; + /** + * Last chunk already sent + */ + private boolean isLastChunkSent; + /** + * The current FileUpload that is currently in encode process + */ + private FileUpload currentFileUpload; + /** + * While adding a FileUpload, is the multipart currently in Mixed Mode + */ + private boolean duringMixedMode; + + /** + * Global Body size + */ + private long globalBodySize; + + /** + * True if this request is a Multipart request + * + * @return True if this request is a Multipart request + */ + public boolean isMultipart() { + return isMultipart; + } + + /** + * Init the delimiter for Global Part (Data). + */ + private void initDataMultipart() { + multipartDataBoundary = getNewMultipartDelimiter(); + } + + /** + * Init the delimiter for Mixed Part (Mixed). + */ + private void initMixedMultipart() { + multipartMixedBoundary = getNewMultipartDelimiter(); + } + + /** + * + * @return a newly generated Delimiter (either for DATA or MIXED) + */ + private static String getNewMultipartDelimiter() { + // construct a generated delimiter + Random random = new Random(); + return Long.toHexString(random.nextLong()).toLowerCase(); + } + + /** + * This method returns a List of all InterfaceHttpData from body part.
+ + * @return the list of InterfaceHttpData from Body part + */ + public List getBodyListAttributes() { + return bodyListDatas; + } + + /** + * Set the Body HttpDatas list + * + * @param datas + * @throws NullPointerException + * for datas + * @throws ErrorDataEncoderException + * if the encoding is in error or if the finalize were already done + */ + public void setBodyHttpDatas(List datas) throws ErrorDataEncoderException { + if (datas == null) { + throw new NullPointerException("datas"); + } + globalBodySize = 0; + bodyListDatas.clear(); + currentFileUpload = null; + duringMixedMode = false; + multipartHttpDatas.clear(); + for (InterfaceHttpData data : datas) { + addBodyHttpData(data); + } + } + + /** + * Add a simple attribute in the body as Name=Value + * + * @param name + * name of the parameter + * @param value + * the value of the parameter + * @throws NullPointerException + * for name + * @throws ErrorDataEncoderException + * if the encoding is in error or if the finalize were already done + */ + public void addBodyAttribute(String name, String value) throws ErrorDataEncoderException { + if (name == null) { + throw new NullPointerException("name"); + } + String svalue = value; + if (value == null) { + svalue = ""; + } + Attribute data = factory.createAttribute(request, name, svalue); + addBodyHttpData(data); + } + + /** + * Add a file as a FileUpload + * + * @param name + * the name of the parameter + * @param file + * the file to be uploaded (if not Multipart mode, only the filename will be included) + * @param contentType + * the associated contentType for the File + * @param isText + * True if this file should be transmitted in Text format (else binary) + * @throws NullPointerException + * for name and file + * @throws ErrorDataEncoderException + * if the encoding is in error or if the finalize were already done + */ + public void addBodyFileUpload(String name, File file, String contentType, boolean isText) + throws ErrorDataEncoderException { + if (name == null) { + throw new NullPointerException("name"); + } + if (file == null) { + throw new NullPointerException("file"); + } + String scontentType = contentType; + String contentTransferEncoding = null; + if (contentType == null) { + if (isText) { + scontentType = HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE; + } else { + scontentType = HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE; + } + } + if (!isText) { + contentTransferEncoding = HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value; + } + FileUpload fileUpload = factory.createFileUpload(request, name, file.getName(), scontentType, + contentTransferEncoding, null, file.length()); + try { + fileUpload.setContent(file); + } catch (IOException e) { + throw new ErrorDataEncoderException(e); + } + addBodyHttpData(fileUpload); + } + + /** + * Add a series of Files associated with one File parameter (implied Mixed mode in Multipart) + * + * @param name + * the name of the parameter + * @param file + * the array of files + * @param contentType + * the array of content Types associated with each file + * @param isText + * the array of isText attribute (False meaning binary mode) for each file + * @throws NullPointerException + * also throws if array have different sizes + * @throws ErrorDataEncoderException + * if the encoding is in error or if the finalize were already done + */ + public void addBodyFileUploads(String name, File[] file, String[] contentType, boolean[] isText) + throws ErrorDataEncoderException { + if (file.length != contentType.length && file.length != isText.length) { + throw new NullPointerException("Different array length"); + } + for (int i = 0; i < file.length; i++) { + addBodyFileUpload(name, file[i], contentType[i], isText[i]); + } + } + + /** + * Add the InterfaceHttpData to the Body list + * + * @param data + * @throws NullPointerException + * for data + * @throws ErrorDataEncoderException + * if the encoding is in error or if the finalize were already done + */ + public void addBodyHttpData(InterfaceHttpData data) throws ErrorDataEncoderException { + if (headerFinalized) { + throw new ErrorDataEncoderException("Cannot add value once finalized"); + } + if (data == null) { + throw new NullPointerException("data"); + } + bodyListDatas.add(data); + if (!isMultipart) { + if (data instanceof Attribute) { + Attribute attribute = (Attribute) data; + try { + // name=value& with encoded name and attribute + String key = encodeAttribute(attribute.getName(), charset); + String value = encodeAttribute(attribute.getValue(), charset); + Attribute newattribute = factory.createAttribute(request, key, value); + multipartHttpDatas.add(newattribute); + globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1; + } catch (IOException e) { + throw new ErrorDataEncoderException(e); + } + } else if (data instanceof FileUpload) { + // since not Multipart, only name=filename => Attribute + FileUpload fileUpload = (FileUpload) data; + // name=filename& with encoded name and filename + String key = encodeAttribute(fileUpload.getName(), charset); + String value = encodeAttribute(fileUpload.getFilename(), charset); + Attribute newattribute = factory.createAttribute(request, key, value); + multipartHttpDatas.add(newattribute); + globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1; + } + return; + } + /* + * Logic: + * if not Attribute: + * add Data to body list + * if (duringMixedMode) + * add endmixedmultipart delimiter + * currentFileUpload = null + * duringMixedMode = false; + * add multipart delimiter, multipart body header and Data to multipart list + * reset currentFileUpload, duringMixedMode + * if FileUpload: take care of multiple file for one field => mixed mode + * if (duringMixeMode) + * if (currentFileUpload.name == data.name) + * add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list + * else + * add endmixedmultipart delimiter, multipart body header and Data to multipart list + * currentFileUpload = data + * duringMixedMode = false; + * else + * if (currentFileUpload.name == data.name) + * change multipart body header of previous file into multipart list to + * mixedmultipart start, mixedmultipart body header + * add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list + * duringMixedMode = true + * else + * add multipart delimiter, multipart body header and Data to multipart list + * currentFileUpload = data + * duringMixedMode = false; + * Do not add last delimiter! Could be: + * if duringmixedmode: endmixedmultipart + endmultipart + * else only endmultipart + */ + if (data instanceof Attribute) { + if (duringMixedMode) { + InternalAttribute internal = new InternalAttribute(); + internal.addValue("\r\n--" + multipartMixedBoundary + "--"); + multipartHttpDatas.add(internal); + multipartMixedBoundary = null; + currentFileUpload = null; + duringMixedMode = false; + } + InternalAttribute internal = new InternalAttribute(); + if (multipartHttpDatas.size() > 0) { + // previously a data field so CRLF + internal.addValue("\r\n"); + } + internal.addValue("--" + multipartDataBoundary + "\r\n"); + // content-disposition: form-data; name="field1" + Attribute attribute = (Attribute) data; + internal.addValue(HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + HttpPostBodyUtil.FORM_DATA + "; " + + HttpPostBodyUtil.NAME + "=\"" + encodeAttribute(attribute.getName(), charset) + "\"\r\n"); + Charset localcharset = attribute.getCharset(); + if (localcharset != null) { + // Content-Type: charset=charset + internal.addValue(HttpHeaders.Names.CONTENT_TYPE + ": " + HttpHeaders.Values.CHARSET + "=" + + localcharset + "\r\n"); + } + // CRLF between body header and data + internal.addValue("\r\n"); + multipartHttpDatas.add(internal); + multipartHttpDatas.add(data); + globalBodySize += attribute.length() + internal.size(); + } else if (data instanceof FileUpload) { + FileUpload fileUpload = (FileUpload) data; + InternalAttribute internal = new InternalAttribute(); + if (multipartHttpDatas.size() > 0) { + // previously a data field so CRLF + internal.addValue("\r\n"); + } + boolean localMixed = false; + if (duringMixedMode) { + if (currentFileUpload != null && currentFileUpload.getName().equals(fileUpload.getName())) { + // continue a mixed mode + + localMixed = true; + } else { + // end a mixed mode + + // add endmixedmultipart delimiter, multipart body header + // and + // Data to multipart list + internal.addValue("--" + multipartMixedBoundary + "--"); + multipartHttpDatas.add(internal); + multipartMixedBoundary = null; + // start a new one (could be replaced if mixed start again + // from here + internal = new InternalAttribute(); + internal.addValue("\r\n"); + localMixed = false; + // new currentFileUpload and no more in Mixed mode + currentFileUpload = fileUpload; + duringMixedMode = false; + } + } else { + if (currentFileUpload != null && currentFileUpload.getName().equals(fileUpload.getName())) { + // create a new mixed mode (from previous file) + + // change multipart body header of previous file into + // multipart list to + // mixedmultipart start, mixedmultipart body header + + // change Internal (size()-2 position in multipartHttpDatas) + // from (line starting with *) + // --AaB03x + // * Content-Disposition: form-data; name="files"; + // filename="file1.txt" + // Content-Type: text/plain + // to (lines starting with *) + // --AaB03x + // * Content-Disposition: form-data; name="files" + // * Content-Type: multipart/mixed; boundary=BbC04y + // * + // * --BbC04y + // * Content-Disposition: file; filename="file1.txt" + // Content-Type: text/plain + initMixedMultipart(); + InternalAttribute pastAttribute = (InternalAttribute) multipartHttpDatas.get(multipartHttpDatas + .size() - 2); + // remove past size + globalBodySize -= pastAttribute.size(); + String replacement = HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + HttpPostBodyUtil.FORM_DATA + + "; " + HttpPostBodyUtil.NAME + "=\"" + encodeAttribute(fileUpload.getName(), charset) + + "\"\r\n"; + replacement += HttpHeaders.Names.CONTENT_TYPE + ": " + HttpPostBodyUtil.MULTIPART_MIXED + "; " + + HttpHeaders.Values.BOUNDARY + "=" + multipartMixedBoundary + "\r\n\r\n"; + replacement += "--" + multipartMixedBoundary + "\r\n"; + replacement += HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + HttpPostBodyUtil.FILE + "; " + + HttpPostBodyUtil.FILENAME + "=\"" + encodeAttribute(fileUpload.getFilename(), charset) + + "\"\r\n"; + pastAttribute.setValue(replacement, 1); + // update past size + globalBodySize += pastAttribute.size(); + + // now continue + // add mixedmultipart delimiter, mixedmultipart body header + // and + // Data to multipart list + localMixed = true; + duringMixedMode = true; + } else { + // a simple new multipart + // add multipart delimiter, multipart body header and Data + // to multipart list + localMixed = false; + currentFileUpload = fileUpload; + duringMixedMode = false; + } + } + + if (localMixed) { + // add mixedmultipart delimiter, mixedmultipart body header and + // Data to multipart list + internal.addValue("--" + multipartMixedBoundary + "\r\n"); + // Content-Disposition: file; filename="file1.txt" + internal.addValue(HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + HttpPostBodyUtil.FILE + "; " + + HttpPostBodyUtil.FILENAME + "=\"" + encodeAttribute(fileUpload.getFilename(), charset) + + "\"\r\n"); + + } else { + internal.addValue("--" + multipartDataBoundary + "\r\n"); + // Content-Disposition: form-data; name="files"; + // filename="file1.txt" + internal.addValue(HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + HttpPostBodyUtil.FORM_DATA + "; " + + HttpPostBodyUtil.NAME + "=\"" + encodeAttribute(fileUpload.getName(), charset) + "\"; " + + HttpPostBodyUtil.FILENAME + "=\"" + encodeAttribute(fileUpload.getFilename(), charset) + + "\"\r\n"); + } + // Content-Type: image/gif + // Content-Type: text/plain; charset=ISO-8859-1 + // Content-Transfer-Encoding: binary + internal.addValue(HttpHeaders.Names.CONTENT_TYPE + ": " + fileUpload.getContentType()); + String contentTransferEncoding = fileUpload.getContentTransferEncoding(); + if (contentTransferEncoding != null + && contentTransferEncoding.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value)) { + internal.addValue("\r\n" + HttpHeaders.Names.CONTENT_TRANSFER_ENCODING + ": " + + HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value + "\r\n\r\n"); + } else if (fileUpload.getCharset() != null) { + internal.addValue("; " + HttpHeaders.Values.CHARSET + "=" + fileUpload.getCharset() + "\r\n\r\n"); + } else { + internal.addValue("\r\n\r\n"); + } + multipartHttpDatas.add(internal); + multipartHttpDatas.add(data); + globalBodySize += fileUpload.length() + internal.size(); + } + } + + /** + * Iterator to be used when encoding will be called chunk after chunk + */ + private ListIterator iterator; + + /** + * Finalize the request by preparing the Header in the request and returns the request ready to be sent.
+ * Once finalized, no data must be added.
+ * If the request does not need chunk (isChunked() == false), this request is the only object to send to the remote + * server. + * + * @return the request object (chunked or not according to size of body) + * @throws ErrorDataEncoderException + * if the encoding is in error or if the finalize were already done + */ + public HttpRequest finalizeRequest() throws ErrorDataEncoderException { + // Finalize the multipartHttpDatas + if (!headerFinalized) { + if (isMultipart) { + InternalAttribute internal = new InternalAttribute(); + if (duringMixedMode) { + internal.addValue("\r\n--" + multipartMixedBoundary + "--"); + } + internal.addValue("\r\n--" + multipartDataBoundary + "--\r\n"); + multipartHttpDatas.add(internal); + multipartMixedBoundary = null; + currentFileUpload = null; + duringMixedMode = false; + globalBodySize += internal.size(); + } + headerFinalized = true; + } else { + throw new ErrorDataEncoderException("Header already encoded"); + } + List contentTypes = request.getHeaders(HttpHeaders.Names.CONTENT_TYPE); + List transferEncoding = request.getHeaders(HttpHeaders.Names.TRANSFER_ENCODING); + if (contentTypes != null) { + request.removeHeader(HttpHeaders.Names.CONTENT_TYPE); + for (String contentType : contentTypes) { + // "multipart/form-data; boundary=--89421926422648" + if (contentType.toLowerCase().startsWith(HttpHeaders.Values.MULTIPART_FORM_DATA)) { + // ignore + } else if (contentType.toLowerCase().startsWith(HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED)) { + // ignore + } else { + request.addHeader(HttpHeaders.Names.CONTENT_TYPE, contentType); + } + } + } + if (isMultipart) { + String value = HttpHeaders.Values.MULTIPART_FORM_DATA + "; " + HttpHeaders.Values.BOUNDARY + "=" + + multipartDataBoundary; + request.addHeader(HttpHeaders.Names.CONTENT_TYPE, value); + } else { + // Not multipart + request.addHeader(HttpHeaders.Names.CONTENT_TYPE, HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED); + } + // Now consider size for chunk or not + long realSize = globalBodySize; + if (isMultipart) { + iterator = multipartHttpDatas.listIterator(); + } else { + realSize -= 1; // last '&' removed + iterator = multipartHttpDatas.listIterator(); + } + request.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(realSize)); + if (realSize > HttpPostBodyUtil.chunkSize || isMultipart) { + isChunked = true; + if (transferEncoding != null) { + request.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING); + for (String v : transferEncoding) { + if (v.equalsIgnoreCase(HttpHeaders.Values.CHUNKED)) { + // ignore + } else { + request.addHeader(HttpHeaders.Names.TRANSFER_ENCODING, v); + } + } + } + request.setTransferEncoding(HttpTransferEncoding.CHUNKED); + request.setContent(EMPTY_BUFFER); + } else { + // get the only one body and set it to the request + HttpChunk chunk = nextChunk(); + request.setContent(chunk.getContent()); + } + return request; + } + + /** + * @return True if the request is by Chunk + */ + public boolean isChunked() { + return isChunked; + } + + /** + * Encode one attribute + * + * @param s + * @param charset + * @return the encoded attribute + * @throws ErrorDataEncoderException + * if the encoding is in error + */ + private static String encodeAttribute(String s, Charset charset) throws ErrorDataEncoderException { + if (s == null) { + return ""; + } + try { + return URLEncoder.encode(s, charset.name()); + } catch (UnsupportedEncodingException e) { + throw new ErrorDataEncoderException(charset.name(), e); + } + } + + /** + * The ByteBuf currently used by the encoder + */ + private ByteBuf currentBuffer; + /** + * The current InterfaceHttpData to encode (used if more chunks are available) + */ + private InterfaceHttpData currentData; + /** + * If not multipart, does the currentBuffer stands for the Key or for the Value + */ + private boolean isKey = true; + + /** + * + * @return the next ByteBuf to send as a HttpChunk and modifying currentBuffer accordingly + */ + private ByteBuf fillByteBuf() { + int length = currentBuffer.readableBytes(); + if (length > HttpPostBodyUtil.chunkSize) { + ByteBuf slice = currentBuffer.slice(currentBuffer.readerIndex(), HttpPostBodyUtil.chunkSize); + currentBuffer.skipBytes(HttpPostBodyUtil.chunkSize); + return slice; + } else { + // to continue + ByteBuf slice = currentBuffer; + currentBuffer = null; + return slice; + } + } + + /** + * From the current context (currentBuffer and currentData), returns the next HttpChunk (if possible) trying to get + * sizeleft bytes more into the currentBuffer. This is the Multipart version. + * + * @param sizeleft + * the number of bytes to try to get from currentData + * @return the next HttpChunk or null if not enough bytes were found + * @throws ErrorDataEncoderException + * if the encoding is in error + */ + private HttpChunk encodeNextChunkMultipart(int sizeleft) throws ErrorDataEncoderException { + if (currentData == null) { + return null; + } + ByteBuf buffer; + if (currentData instanceof InternalAttribute) { + String internal = ((InternalAttribute) currentData).toString(); + byte[] bytes; + try { + bytes = internal.getBytes("ASCII"); + } catch (UnsupportedEncodingException e) { + throw new ErrorDataEncoderException(e); + } + buffer = wrappedBuffer(bytes); + currentData = null; + } else { + if (currentData instanceof Attribute) { + try { + buffer = ((Attribute) currentData).getChunk(sizeleft); + } catch (IOException e) { + throw new ErrorDataEncoderException(e); + } + } else { + try { + buffer = ((FileUpload) currentData).getChunk(sizeleft); + } catch (IOException e) { + throw new ErrorDataEncoderException(e); + } + } + if (buffer.capacity() == 0) { + // end for current InterfaceHttpData, need more data + currentData = null; + return null; + } + } + if (currentBuffer == null) { + currentBuffer = buffer; + } else { + currentBuffer = wrappedBuffer(currentBuffer, buffer); + } + if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) { + currentData = null; + return null; + } + buffer = fillByteBuf(); + return new DefaultHttpChunk(buffer); + } + + /** + * From the current context (currentBuffer and currentData), returns the next HttpChunk (if possible) trying to get + * sizeleft bytes more into the currentBuffer. This is the UrlEncoded version. + * + * @param sizeleft + * the number of bytes to try to get from currentData + * @return the next HttpChunk or null if not enough bytes were found + * @throws ErrorDataEncoderException + * if the encoding is in error + */ + private HttpChunk encodeNextChunkUrlEncoded(int sizeleft) throws ErrorDataEncoderException { + if (currentData == null) { + return null; + } + int size = sizeleft; + ByteBuf buffer; + + // Set name= + if (isKey) { + String key = currentData.getName(); + buffer = wrappedBuffer(key.getBytes()); + isKey = false; + if (currentBuffer == null) { + currentBuffer = wrappedBuffer(buffer, wrappedBuffer("=".getBytes())); + // continue + size -= buffer.readableBytes() + 1; + } else { + currentBuffer = wrappedBuffer(currentBuffer, buffer, wrappedBuffer("=".getBytes())); + // continue + size -= buffer.readableBytes() + 1; + } + if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) { + buffer = fillByteBuf(); + return new DefaultHttpChunk(buffer); + } + } + + // Put value into buffer + try { + buffer = ((Attribute) currentData).getChunk(size); + } catch (IOException e) { + throw new ErrorDataEncoderException(e); + } + + // Figure out delimiter + ByteBuf delimiter = null; + if (buffer.readableBytes() < size) { + isKey = true; + delimiter = iterator.hasNext() ? wrappedBuffer("&".getBytes()) : null; + } + + // End for current InterfaceHttpData, need potentially more data + if (buffer.capacity() == 0) { + currentData = null; + if (currentBuffer == null) { + currentBuffer = delimiter; + } else { + if (delimiter != null) { + currentBuffer = wrappedBuffer(currentBuffer, delimiter); + } + } + if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) { + buffer = fillByteBuf(); + return new DefaultHttpChunk(buffer); + } + return null; + } + + // Put it all together: name=value& + if (currentBuffer == null) { + if (delimiter != null) { + currentBuffer = wrappedBuffer(buffer, delimiter); + } else { + currentBuffer = buffer; + } + } else { + if (delimiter != null) { + currentBuffer = wrappedBuffer(currentBuffer, buffer, delimiter); + } else { + currentBuffer = wrappedBuffer(currentBuffer, buffer); + } + } + + // end for current InterfaceHttpData, need more data + if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) { + currentData = null; + isKey = true; + return null; + } + + buffer = fillByteBuf(); + return new DefaultHttpChunk(buffer); + } + + public void close() throws Exception { + // NO since the user can want to reuse (broadcast for instance) + // cleanFiles(); + } + + /** + * Returns the next available HttpChunk. The caller is responsible to test if this chunk is the last one (isLast()), + * in order to stop calling this method. + * + * @return the next available HttpChunk + * @throws ErrorDataEncoderException + * if the encoding is in error + */ + public boolean readChunk(MessageBuf buffer) throws ErrorDataEncoderException { + if (isLastChunkSent) { + return false; + } else { + buffer.add(nextChunk()); + return true; + } + } + + /** + * Returns the next available HttpChunk. The caller is responsible to test if this chunk is the last one (isLast()), + * in order to stop calling this method. + * + * @return the next available HttpChunk + * @throws ErrorDataEncoderException + * if the encoding is in error + */ + private HttpChunk nextChunk() throws ErrorDataEncoderException { + if (isLastChunk) { + isLastChunkSent = true; + return new DefaultHttpChunk(EMPTY_BUFFER); + } + ByteBuf buffer = null; + int size = HttpPostBodyUtil.chunkSize; + // first test if previous buffer is not empty + if (currentBuffer != null) { + size -= currentBuffer.readableBytes(); + } + if (size <= 0) { + // NextChunk from buffer + buffer = fillByteBuf(); + return new DefaultHttpChunk(buffer); + } + // size > 0 + if (currentData != null) { + // continue to read data + if (isMultipart) { + HttpChunk chunk = encodeNextChunkMultipart(size); + if (chunk != null) { + return chunk; + } + } else { + HttpChunk chunk = encodeNextChunkUrlEncoded(size); + if (chunk != null) { + // NextChunk Url from currentData + return chunk; + } + } + size = HttpPostBodyUtil.chunkSize - currentBuffer.readableBytes(); + } + if (!iterator.hasNext()) { + isLastChunk = true; + // NextChunk as last non empty from buffer + buffer = currentBuffer; + currentBuffer = null; + return new DefaultHttpChunk(buffer); + } + while (size > 0 && iterator.hasNext()) { + currentData = iterator.next(); + HttpChunk chunk; + if (isMultipart) { + chunk = encodeNextChunkMultipart(size); + } else { + chunk = encodeNextChunkUrlEncoded(size); + } + if (chunk == null) { + // not enough + size = HttpPostBodyUtil.chunkSize - currentBuffer.readableBytes(); + continue; + } + // NextChunk from data + return chunk; + } + // end since no more data + isLastChunk = true; + if (currentBuffer == null) { + isLastChunkSent = true; + // LastChunk with no more data + return new DefaultHttpChunk(EMPTY_BUFFER); + } + // Previous LastChunk with no more data + buffer = currentBuffer; + currentBuffer = null; + return new DefaultHttpChunk(buffer); + } + + public boolean isEndOfInput() throws Exception { + return isLastChunkSent; + } + + /** + * Exception when an error occurs while encoding + */ + public static class ErrorDataEncoderException extends Exception { + private static final long serialVersionUID = 5020247425493164465L; + + public ErrorDataEncoderException() { + } + + /** + * @param arg0 + */ + public ErrorDataEncoderException(String arg0) { + super(arg0); + } + + /** + * @param arg0 + */ + public ErrorDataEncoderException(Throwable arg0) { + super(arg0); + } + + /** + * @param arg0 + * @param arg1 + */ + public ErrorDataEncoderException(String arg0, Throwable arg1) { + super(arg0, arg1); + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InterfaceHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InterfaceHttpData.java new file mode 100644 index 0000000000..12f1ab45ff --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InterfaceHttpData.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +/** + * Interface for all Objects that could be encoded/decoded using HttpPostRequestEncoder/Decoder + */ +public interface InterfaceHttpData extends Comparable { + enum HttpDataType { + Attribute, FileUpload, InternalAttribute + } + + /** + * Returns the name of this InterfaceHttpData. + */ + String getName(); + + /** + * + * @return The HttpDataType + */ + HttpDataType getHttpDataType(); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java new file mode 100644 index 0000000000..eb35ec884d --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.util.ArrayList; +import java.util.List; + +/** + * This Attribute is only for Encoder use to insert special command between object if needed + * (like Multipart Mixed mode) + */ +public class InternalAttribute implements InterfaceHttpData { + protected List value = new ArrayList(); + + public HttpDataType getHttpDataType() { + return HttpDataType.InternalAttribute; + } + + public List getValue() { + return value; + } + + public void addValue(String value) { + if (value == null) { + throw new NullPointerException("value"); + } + this.value.add(value); + } + + public void addValue(String value, int rank) { + if (value == null) { + throw new NullPointerException("value"); + } + this.value.add(rank, value); + } + + public void setValue(String value, int rank) { + if (value == null) { + throw new NullPointerException("value"); + } + this.value.set(rank, value); + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Attribute)) { + return false; + } + Attribute attribute = (Attribute) o; + return getName().equalsIgnoreCase(attribute.getName()); + } + + public int compareTo(InterfaceHttpData arg0) { + if (!(arg0 instanceof InternalAttribute)) { + throw new ClassCastException("Cannot compare " + getHttpDataType() + + " with " + arg0.getHttpDataType()); + } + return compareTo((InternalAttribute) arg0); + } + + public int compareTo(InternalAttribute o) { + return getName().compareToIgnoreCase(o.getName()); + } + + public int size() { + int size = 0; + for (String elt : value) { + size += elt.length(); + } + return size; + } + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + for (String elt : value) { + result.append(elt); + } + return result.toString(); + } + + public String getName() { + return "InternalAttribute"; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java new file mode 100644 index 0000000000..ba26cb64dd --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.io.IOException; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpConstants; +import static io.netty.buffer.Unpooled.*; + +/** + * Memory implementation of Attributes + */ +public class MemoryAttribute extends AbstractMemoryHttpData implements Attribute { + + public MemoryAttribute(String name) { + super(name, HttpConstants.DEFAULT_CHARSET, 0); + } + /** + * + * @param name + * @param value + * @throws NullPointerException + * @throws IllegalArgumentException + * @throws IOException + */ + public MemoryAttribute(String name, String value) throws IOException { + super(name, HttpConstants.DEFAULT_CHARSET, 0); // Attribute have no default size + setValue(value); + } + + public HttpDataType getHttpDataType() { + return HttpDataType.Attribute; + } + + public String getValue() { + return getByteBuf().toString(charset); + } + + public void setValue(String value) throws IOException { + if (value == null) { + throw new NullPointerException("value"); + } + byte [] bytes = value.getBytes(charset.name()); + ByteBuf buffer = wrappedBuffer(bytes); + if (definedSize > 0) { + definedSize = buffer.readableBytes(); + } + setContent(buffer); + } + + @Override + public void addContent(ByteBuf buffer, boolean last) throws IOException { + int localsize = buffer.readableBytes(); + if (definedSize > 0 && definedSize < size + localsize) { + definedSize = size + localsize; + } + super.addContent(buffer, last); + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Attribute)) { + return false; + } + Attribute attribute = (Attribute) o; + return getName().equalsIgnoreCase(attribute.getName()); + } + + public int compareTo(InterfaceHttpData arg0) { + if (!(arg0 instanceof Attribute)) { + throw new ClassCastException("Cannot compare " + getHttpDataType() + + " with " + arg0.getHttpDataType()); + } + return compareTo((Attribute) arg0); + } + + public int compareTo(Attribute o) { + return getName().compareToIgnoreCase(o.getName()); + } + + @Override + public String toString() { + return getName() + "=" + getValue(); + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java new file mode 100644 index 0000000000..a7e3b4c342 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import java.nio.charset.Charset; + +import io.netty.handler.codec.http.HttpHeaders; + +/** + * Default FileUpload implementation that stores file into memory.

+ * + * Warning: be aware of the memory limitation. + */ +public class MemoryFileUpload extends AbstractMemoryHttpData implements FileUpload { + + private String filename; + + private String contentType; + + private String contentTransferEncoding; + + public MemoryFileUpload(String name, String filename, String contentType, + String contentTransferEncoding, Charset charset, long size) { + super(name, charset, size); + setFilename(filename); + setContentType(contentType); + setContentTransferEncoding(contentTransferEncoding); + } + + public HttpDataType getHttpDataType() { + return HttpDataType.FileUpload; + } + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + if (filename == null) { + throw new NullPointerException("filename"); + } + this.filename = filename; + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Attribute)) { + return false; + } + Attribute attribute = (Attribute) o; + return getName().equalsIgnoreCase(attribute.getName()); + } + + public int compareTo(InterfaceHttpData arg0) { + if (!(arg0 instanceof FileUpload)) { + throw new ClassCastException("Cannot compare " + getHttpDataType() + + " with " + arg0.getHttpDataType()); + } + return compareTo((FileUpload) arg0); + } + + public int compareTo(FileUpload o) { + int v; + v = getName().compareToIgnoreCase(o.getName()); + if (v != 0) { + return v; + } + // TODO should we compare size for instance ? + return v; + } + + public void setContentType(String contentType) { + if (contentType == null) { + throw new NullPointerException("contentType"); + } + this.contentType = contentType; + } + + public String getContentType() { + return contentType; + } + + public String getContentTransferEncoding() { + return contentTransferEncoding; + } + + public void setContentTransferEncoding(String contentTransferEncoding) { + this.contentTransferEncoding = contentTransferEncoding; + } + + @Override + public String toString() { + return HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + + HttpPostBodyUtil.FORM_DATA + "; " + HttpPostBodyUtil.NAME + "=\"" + getName() + + "\"; " + HttpPostBodyUtil.FILENAME + "=\"" + filename + "\"\r\n" + + HttpHeaders.Names.CONTENT_TYPE + ": " + contentType + + (charset != null? "; " + HttpHeaders.Values.CHARSET + "=" + charset + "\r\n" : "\r\n") + + HttpHeaders.Names.CONTENT_LENGTH + ": " + length() + "\r\n" + + "Completed: " + isCompleted() + + "\r\nIsInMemory: " + isInMemory(); + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedAttribute.java new file mode 100644 index 0000000000..dd5fd27abe --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedAttribute.java @@ -0,0 +1,180 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import io.netty.buffer.ByteBuf; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * Mixed implementation using both in Memory and in File with a limit of size + */ +public class MixedAttribute implements Attribute { + private Attribute attribute; + + private final long limitSize; + + public MixedAttribute(String name, long limitSize) { + this.limitSize = limitSize; + attribute = new MemoryAttribute(name); + } + + public MixedAttribute(String name, String value, long limitSize) { + this.limitSize = limitSize; + if (value.length() > this.limitSize) { + try { + attribute = new DiskAttribute(name, value); + } catch (IOException e) { + // revert to Memory mode + try { + attribute = new MemoryAttribute(name, value); + } catch (IOException e1) { + throw new IllegalArgumentException(e); + } + } + } else { + try { + attribute = new MemoryAttribute(name, value); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + } + + public void addContent(ByteBuf buffer, boolean last) throws IOException { + if (attribute instanceof MemoryAttribute) { + if (attribute.length() + buffer.readableBytes() > limitSize) { + DiskAttribute diskAttribute = new DiskAttribute(attribute + .getName()); + if (((MemoryAttribute) attribute).getByteBuf() != null) { + diskAttribute.addContent(((MemoryAttribute) attribute) + .getByteBuf(), false); + } + attribute = diskAttribute; + } + } + attribute.addContent(buffer, last); + } + + public void delete() { + attribute.delete(); + } + + public byte[] get() throws IOException { + return attribute.get(); + } + + public ByteBuf getByteBuf() throws IOException { + return attribute.getByteBuf(); + } + + public Charset getCharset() { + return attribute.getCharset(); + } + + public String getString() throws IOException { + return attribute.getString(); + } + + public String getString(Charset encoding) throws IOException { + return attribute.getString(encoding); + } + + public boolean isCompleted() { + return attribute.isCompleted(); + } + + public boolean isInMemory() { + return attribute.isInMemory(); + } + + public long length() { + return attribute.length(); + } + + public boolean renameTo(File dest) throws IOException { + return attribute.renameTo(dest); + } + + public void setCharset(Charset charset) { + attribute.setCharset(charset); + } + + public void setContent(ByteBuf buffer) throws IOException { + if (buffer.readableBytes() > limitSize) { + if (attribute instanceof MemoryAttribute) { + // change to Disk + attribute = new DiskAttribute(attribute.getName()); + } + } + attribute.setContent(buffer); + } + + public void setContent(File file) throws IOException { + if (file.length() > limitSize) { + if (attribute instanceof MemoryAttribute) { + // change to Disk + attribute = new DiskAttribute(attribute.getName()); + } + } + attribute.setContent(file); + } + + public void setContent(InputStream inputStream) throws IOException { + if (attribute instanceof MemoryAttribute) { + // change to Disk even if we don't know the size + attribute = new DiskAttribute(attribute.getName()); + } + attribute.setContent(inputStream); + } + + public HttpDataType getHttpDataType() { + return attribute.getHttpDataType(); + } + + public String getName() { + return attribute.getName(); + } + + public int compareTo(InterfaceHttpData o) { + return attribute.compareTo(o); + } + + @Override + public String toString() { + return "Mixed: " + attribute.toString(); + } + + public String getValue() throws IOException { + return attribute.getValue(); + } + + public void setValue(String value) throws IOException { + attribute.setValue(value); + } + + public ByteBuf getChunk(int length) throws IOException { + return attribute.getChunk(length); + } + + public File getFile() throws IOException { + return attribute.getFile(); + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedFileUpload.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedFileUpload.java new file mode 100644 index 0000000000..e0d6de5dfd --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MixedFileUpload.java @@ -0,0 +1,201 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.handler.codec.http.multipart; + +import io.netty.buffer.ByteBuf; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * Mixed implementation using both in Memory and in File with a limit of size + */ +public class MixedFileUpload implements FileUpload { + private FileUpload fileUpload; + + private final long limitSize; + + private final long definedSize; + + public MixedFileUpload(String name, String filename, String contentType, + String contentTransferEncoding, Charset charset, long size, + long limitSize) { + this.limitSize = limitSize; + if (size > this.limitSize) { + fileUpload = new DiskFileUpload(name, filename, contentType, + contentTransferEncoding, charset, size); + } else { + fileUpload = new MemoryFileUpload(name, filename, contentType, + contentTransferEncoding, charset, size); + } + definedSize = size; + } + + public void addContent(ByteBuf buffer, boolean last) + throws IOException { + if (fileUpload instanceof MemoryFileUpload) { + if (fileUpload.length() + buffer.readableBytes() > limitSize) { + DiskFileUpload diskFileUpload = new DiskFileUpload(fileUpload + .getName(), fileUpload.getFilename(), fileUpload + .getContentType(), fileUpload + .getContentTransferEncoding(), fileUpload.getCharset(), + definedSize); + if (((MemoryFileUpload) fileUpload).getByteBuf() != null) { + diskFileUpload.addContent(((MemoryFileUpload) fileUpload) + .getByteBuf(), false); + } + fileUpload = diskFileUpload; + } + } + fileUpload.addContent(buffer, last); + } + + public void delete() { + fileUpload.delete(); + } + + public byte[] get() throws IOException { + return fileUpload.get(); + } + + public ByteBuf getByteBuf() throws IOException { + return fileUpload.getByteBuf(); + } + + public Charset getCharset() { + return fileUpload.getCharset(); + } + + public String getContentType() { + return fileUpload.getContentType(); + } + + public String getContentTransferEncoding() { + return fileUpload.getContentTransferEncoding(); + } + + public String getFilename() { + return fileUpload.getFilename(); + } + + public String getString() throws IOException { + return fileUpload.getString(); + } + + public String getString(Charset encoding) throws IOException { + return fileUpload.getString(encoding); + } + + public boolean isCompleted() { + return fileUpload.isCompleted(); + } + + public boolean isInMemory() { + return fileUpload.isInMemory(); + } + + public long length() { + return fileUpload.length(); + } + + public boolean renameTo(File dest) throws IOException { + return fileUpload.renameTo(dest); + } + + public void setCharset(Charset charset) { + fileUpload.setCharset(charset); + } + + public void setContent(ByteBuf buffer) throws IOException { + if (buffer.readableBytes() > limitSize) { + if (fileUpload instanceof MemoryFileUpload) { + // change to Disk + fileUpload = new DiskFileUpload(fileUpload + .getName(), fileUpload.getFilename(), fileUpload + .getContentType(), fileUpload + .getContentTransferEncoding(), fileUpload.getCharset(), + definedSize); + } + } + fileUpload.setContent(buffer); + } + + public void setContent(File file) throws IOException { + if (file.length() > limitSize) { + if (fileUpload instanceof MemoryFileUpload) { + // change to Disk + fileUpload = new DiskFileUpload(fileUpload + .getName(), fileUpload.getFilename(), fileUpload + .getContentType(), fileUpload + .getContentTransferEncoding(), fileUpload.getCharset(), + definedSize); + } + } + fileUpload.setContent(file); + } + + public void setContent(InputStream inputStream) throws IOException { + if (fileUpload instanceof MemoryFileUpload) { + // change to Disk + fileUpload = new DiskFileUpload(fileUpload + .getName(), fileUpload.getFilename(), fileUpload + .getContentType(), fileUpload + .getContentTransferEncoding(), fileUpload.getCharset(), + definedSize); + } + fileUpload.setContent(inputStream); + } + + public void setContentType(String contentType) { + fileUpload.setContentType(contentType); + } + + public void setContentTransferEncoding(String contentTransferEncoding) { + fileUpload.setContentTransferEncoding(contentTransferEncoding); + } + + public void setFilename(String filename) { + fileUpload.setFilename(filename); + } + + public HttpDataType getHttpDataType() { + return fileUpload.getHttpDataType(); + } + + public String getName() { + return fileUpload.getName(); + } + + public int compareTo(InterfaceHttpData o) { + return fileUpload.compareTo(o); + } + + @Override + public String toString() { + return "Mixed: " + fileUpload.toString(); + } + + public ByteBuf getChunk(int length) throws IOException { + return fileUpload.getChunk(length); + } + + public File getFile() throws IOException { + return fileUpload.getFile(); + } + +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/package-info.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/package-info.java new file mode 100644 index 0000000000..bd53ddbb7a --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ + +/** + * HTTP multipart support. + */ +package io.netty.handler.codec.http.multipart; diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java new file mode 100644 index 0000000000..9a59de4fea --- /dev/null +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java @@ -0,0 +1,974 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.example.http.upload; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.socket.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.ClientCookieEncoder; +import io.netty.handler.codec.http.DefaultCookie; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.QueryStringEncoder; +import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; +import io.netty.handler.codec.http.multipart.DiskAttribute; +import io.netty.handler.codec.http.multipart.DiskFileUpload; +import io.netty.handler.codec.http.multipart.HttpDataFactory; +import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder; +import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder.ErrorDataEncoderException; +import io.netty.handler.codec.http.multipart.InterfaceHttpData; +import io.netty.logging.InternalLogger; +import io.netty.logging.InternalLoggerFactory; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map.Entry; + +/** + * This class is meant to be run against {@link HttpUploadServer}. + */ +public class HttpUploadClient { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(HttpUploadClient.class); + + private final String baseUri; + private final String filePath; + + public HttpUploadClient(String baseUri, String filePath) { + this.baseUri = baseUri; + this.filePath = filePath; + } + + public void run() throws Exception { + String postSimple, postFile, get; + if (baseUri.endsWith("/")) { + postSimple = baseUri + "formpost"; + postFile = baseUri + "formpostmultipart"; + get = baseUri + "formget"; + } else { + postSimple = baseUri + "/formpost"; + postFile = baseUri + "/formpostmultipart"; + get = baseUri + "/formget"; + } + URI uriSimple; + try { + uriSimple = new URI(postSimple); + } catch (URISyntaxException e) { + logger.error("Invalid URI syntax" + e.getCause()); + return; + } + String scheme = uriSimple.getScheme() == null ? "http" : uriSimple.getScheme(); + String host = uriSimple.getHost() == null ? "localhost" : uriSimple.getHost(); + int port = uriSimple.getPort(); + if (port == -1) { + if (scheme.equalsIgnoreCase("http")) { + port = 80; + } else if (scheme.equalsIgnoreCase("https")) { + port = 443; + } + } + + if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) { + logger.error("Only HTTP(S) is supported."); + return; + } + + boolean ssl = scheme.equalsIgnoreCase("https"); + + URI uriFile; + try { + uriFile = new URI(postFile); + } catch (URISyntaxException e) { + logger.error("Error: " + e.getMessage()); + return; + } + File file = new File(filePath); + if (!file.canRead()) { + logger.error("A correct path is needed"); + return; + } + + // Configure the client. + Bootstrap b = new Bootstrap(); + b.group(new NioEventLoopGroup()).channel(NioSocketChannel.class).handler(new HttpUploadClientIntializer(ssl)); + + // setup the factory: here using a mixed memory/disk based on size threshold + HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); // Disk if MINSIZE exceed + + DiskFileUpload.deleteOnExitTemporaryFile = true; // should delete file on exit (in normal exit) + DiskFileUpload.baseDirectory = null; // system temp directory + DiskAttribute.deleteOnExitTemporaryFile = true; // should delete file on exit (in normal exit) + DiskAttribute.baseDirectory = null; // system temp directory + + // Simple Get form: no factory used (not usable) + List> headers = formGet(b, host, port, get, uriSimple); + if (headers == null) { + factory.cleanAllHttpDatas(); + return; + } + + // Simple Post form: factory used for big attributes + List bodylist = formPost(b, host, port, uriSimple, file, factory, headers); + if (bodylist == null) { + factory.cleanAllHttpDatas(); + return; + } + + // Multipart Post form: factory used + formPostMultipart(b, host, port, uriFile, factory, headers, bodylist); + + // Shut down executor threads to exit. + b.shutdown(); + + // Really clean all temporary files if they still exist + factory.cleanAllHttpDatas(); + } + + /** + * Standard usage of HTTP API in Netty without file Upload (get is not able to achieve File upload due to limitation + * on request size). + * + * @return the list of headers that will be used in every example after + **/ + private static List> formGet(Bootstrap bootstrap, String host, int port, String get, + URI uriSimple) throws Exception { + // Start the connection attempt. + // No use of HttpPostRequestEncoder since not a POST + bootstrap.remoteAddress(new InetSocketAddress(host, port)); + Channel channel = bootstrap.connect().sync().channel(); + + // Prepare the HTTP request. + QueryStringEncoder encoder = new QueryStringEncoder(get); + // add Form attribute + encoder.addParam("getform", "GET"); + encoder.addParam("info", "first value"); + encoder.addParam("secondinfo", "secondvalue ���&"); + // not the big one since it is not compatible with GET size + // encoder.addParam("thirdinfo", textArea); + encoder.addParam("thirdinfo", "third value\r\ntest second line\r\n\r\nnew line\r\n"); + encoder.addParam("Send", "Send"); + + URI uriGet; + try { + uriGet = new URI(encoder.toString()); + } catch (URISyntaxException e) { + logger.error("Error: " + e.getMessage()); + return null; + } + + HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uriGet.toASCIIString()); + request.setHeader(HttpHeaders.Names.HOST, host); + request.setHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE); + request.setHeader(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP + "," + + HttpHeaders.Values.DEFLATE); + + request.setHeader(HttpHeaders.Names.ACCEPT_CHARSET, "ISO-8859-1,utf-8;q=0.7,*;q=0.7"); + request.setHeader(HttpHeaders.Names.ACCEPT_LANGUAGE, "fr"); + request.setHeader(HttpHeaders.Names.REFERER, uriSimple.toString()); + request.setHeader(HttpHeaders.Names.USER_AGENT, "Netty Simple Http Client side"); + request.setHeader(HttpHeaders.Names.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); + + // connection will not close but needed + // request.setHeader("Connection","keep-alive"); + // request.setHeader("Keep-Alive","300"); + + request.setHeader(HttpHeaders.Names.COOKIE, ClientCookieEncoder.encode(new DefaultCookie("my-cookie", "foo"), + new DefaultCookie("another-cookie", "bar"))); + + // send request + List> headers = request.getHeaders(); + channel.write(request); + + // Wait for the server to close the connection. + channel.closeFuture().sync(); + + return headers; + } + + /** + * Standard post without multipart but already support on Factory (memory management) + * + * @return the list of HttpData object (attribute and file) to be reused on next post + */ + private static List formPost(Bootstrap bootstrap, String host, int port, URI uriSimple, + File file, HttpDataFactory factory, List> headers) throws Exception { + + // Start the connection attempt + bootstrap.remoteAddress(new InetSocketAddress(host, port)); + Channel channel = bootstrap.connect().sync().channel(); + + // Prepare the HTTP request. + HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uriSimple.toASCIIString()); + + // Use the PostBody encoder + HttpPostRequestEncoder bodyRequestEncoder = null; + try { + bodyRequestEncoder = new HttpPostRequestEncoder(factory, request, false); // false not multipart + } catch (NullPointerException e) { + // should not be since args are not null + e.printStackTrace(); + } catch (ErrorDataEncoderException e) { + // test if method is a POST method + e.printStackTrace(); + } + + // it is legal to add directly header or cookie into the request until finalize + for (Entry entry : headers) { + request.setHeader(entry.getKey(), entry.getValue()); + } + + // add Form attribute + try { + bodyRequestEncoder.addBodyAttribute("getform", "POST"); + bodyRequestEncoder.addBodyAttribute("info", "first value"); + bodyRequestEncoder.addBodyAttribute("secondinfo", "secondvalue ���&"); + bodyRequestEncoder.addBodyAttribute("thirdinfo", textArea); + bodyRequestEncoder.addBodyFileUpload("myfile", file, "application/x-zip-compressed", false); + bodyRequestEncoder.addBodyAttribute("Send", "Send"); + } catch (NullPointerException e) { + // should not be since not null args + e.printStackTrace(); + } catch (ErrorDataEncoderException e) { + // if an encoding error occurs + e.printStackTrace(); + } + + // finalize request + try { + request = bodyRequestEncoder.finalizeRequest(); + } catch (ErrorDataEncoderException e) { + // if an encoding error occurs + e.printStackTrace(); + } + + // Create the bodylist to be reused on the last version with Multipart support + List bodylist = bodyRequestEncoder.getBodyListAttributes(); + + // send request + channel.write(request); + + // test if request was chunked and if so, finish the write + if (bodyRequestEncoder.isChunked()) { + // could do either request.isChunked() + // either do it through ChunkedWriteHandler + channel.write(bodyRequestEncoder).awaitUninterruptibly(); + } + + // Do not clear here since we will reuse the InterfaceHttpData on the + // next request + // for the example (limit action on client side). Take this as a + // broadcast of the same + // request on both Post actions. + // + // On standard program, it is clearly recommended to clean all files + // after each request + // bodyRequestEncoder.cleanFiles(); + + // Wait for the server to close the connection. + channel.closeFuture().sync(); + + return bodylist; + } + + /** + * Multipart example + */ + private static void formPostMultipart(Bootstrap bootstrap, String host, int port, URI uriFile, + HttpDataFactory factory, List> headers, List bodylist) + throws Exception { + + // Start the connection attempt + bootstrap.remoteAddress(new InetSocketAddress(host, port)); + Channel channel = bootstrap.connect().sync().channel(); + + // Prepare the HTTP request. + HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uriFile.toASCIIString()); + + // Use the PostBody encoder + HttpPostRequestEncoder bodyRequestEncoder = null; + try { + bodyRequestEncoder = new HttpPostRequestEncoder(factory, request, true); // true => multipart + } catch (NullPointerException e) { + // should not be since no null args + e.printStackTrace(); + } catch (ErrorDataEncoderException e) { + // test if method is a POST method + e.printStackTrace(); + } + + // it is legal to add directly header or cookie into the request until finalize + for (Entry entry : headers) { + request.setHeader(entry.getKey(), entry.getValue()); + } + + // add Form attribute from previous request in formpost() + try { + bodyRequestEncoder.setBodyHttpDatas(bodylist); + } catch (NullPointerException e1) { + // should not be since previously created + e1.printStackTrace(); + } catch (ErrorDataEncoderException e1) { + // again should not be since previously encoded (except if an error + // occurs previously) + e1.printStackTrace(); + } + + // finalize request + try { + bodyRequestEncoder.finalizeRequest(); + } catch (ErrorDataEncoderException e) { + // if an encoding error occurs + e.printStackTrace(); + } + + // send request + channel.write(request); + + // test if request was chunked and if so, finish the write + if (bodyRequestEncoder.isChunked()) { + channel.write(bodyRequestEncoder).awaitUninterruptibly(); + } + + // Now no more use of file representation (and list of HttpData) + bodyRequestEncoder.cleanFiles(); + + // Wait for the server to close the connection. + channel.closeFuture().sync(); + } + + public static void main(String[] args) throws Exception { + String baseUri; + String filePath; + if (args.length == 2) { + baseUri = args[0]; + filePath = args[1]; + } else { + baseUri = "http://localhost:8080"; + + File f = File.createTempFile("upload", ".txt"); + BufferedWriter bw = new BufferedWriter(new FileWriter(f)); + bw.write("Some text data in a file to be posted"); + bw.close(); + filePath = f.getPath(); + f.deleteOnExit(); + } + + logger.info("Posting to " + baseUri + ". Using file " + filePath); + new HttpUploadClient(baseUri, filePath).run(); + } + + // use to simulate a small TEXTAREA field in a form + private static final String textArea = "short text"; + // use to simulate a big TEXTAREA field in a form + /* + private static final String textAreaLong = + "lkjlkjlKJLKJLKJLKJLJlkj lklkj\r\n\r\nLKJJJJJJJJKKKKKKKKKKKKKKK ����&\r\n\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n" + + "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\r\n"; + */ +} diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadClientHandler.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadClientHandler.java new file mode 100644 index 0000000000..082da90628 --- /dev/null +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadClientHandler.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.example.http.upload; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundMessageHandlerAdapter; +import io.netty.handler.codec.http.HttpChunk; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.logging.InternalLogger; +import io.netty.logging.InternalLoggerFactory; +import io.netty.util.CharsetUtil; + +/** + * Handler that just dumps the contents of the response from the server + */ +public class HttpUploadClientHandler extends ChannelInboundMessageHandlerAdapter { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(HttpUploadClientHandler.class); + + private boolean readingChunks; + + @Override + public void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!readingChunks) { + HttpResponse response = (HttpResponse) msg; + + logger.info("STATUS: " + response.getStatus()); + logger.info("VERSION: " + response.getProtocolVersion()); + + if (!response.getHeaderNames().isEmpty()) { + for (String name : response.getHeaderNames()) { + for (String value : response.getHeaders(name)) { + logger.info("HEADER: " + name + " = " + value); + } + } + } + + if (response.getStatus().getCode() == 200 && response.getTransferEncoding().isMultiple()) { + readingChunks = true; + logger.info("CHUNKED CONTENT {"); + } else { + ByteBuf content = response.getContent(); + if (content.readable()) { + logger.info("CONTENT {"); + logger.info(content.toString(CharsetUtil.UTF_8)); + logger.info("} END OF CONTENT"); + } + } + } else { + HttpChunk chunk = (HttpChunk) msg; + if (chunk.isLast()) { + readingChunks = false; + logger.info("} END OF CHUNKED CONTENT"); + } else { + logger.info(chunk.getContent().toString(CharsetUtil.UTF_8)); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + cause.printStackTrace(); + ctx.channel().close(); + } +} diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadClientIntializer.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadClientIntializer.java new file mode 100644 index 0000000000..1e7eaa6b5c --- /dev/null +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadClientIntializer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.example.http.upload; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.example.securechat.SecureChatSslContextFactory; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.stream.ChunkedWriteHandler; + +import javax.net.ssl.SSLEngine; + +public class HttpUploadClientIntializer extends ChannelInitializer { + private final boolean ssl; + + public HttpUploadClientIntializer(boolean ssl) { + this.ssl = ssl; + } + + @Override + public void initChannel(SocketChannel ch) throws Exception { + // Create a default pipeline implementation. + ChannelPipeline pipeline = ch.pipeline(); + + if (ssl) { + SSLEngine engine = SecureChatSslContextFactory.getClientContext().createSSLEngine(); + engine.setUseClientMode(true); + pipeline.addLast("ssl", new SslHandler(engine)); + } + + pipeline.addLast("codec", new HttpClientCodec()); + + // Remove the following line if you don't want automatic content decompression. + pipeline.addLast("inflater", new HttpContentDecompressor()); + + // to be used since huge file transfer + pipeline.addLast("chunkedWriter", new ChunkedWriteHandler()); + + pipeline.addLast("handler", new HttpUploadClientHandler()); + } +} diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java new file mode 100644 index 0000000000..d5bfa007af --- /dev/null +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.example.http.upload; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.socket.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; + +/** + * A HTTP server showing how to use the HTTP multipart package for file uploads and decoding post data. + */ +public class HttpUploadServer { + + private final int port; + public static boolean isSSL; + + public HttpUploadServer(int port) { + this.port = port; + } + + public void run() throws Exception { + ServerBootstrap b = new ServerBootstrap(); + try { + b.group(new NioEventLoopGroup(), new NioEventLoopGroup()).channel(NioServerSocketChannel.class) + .localAddress(port).childHandler(new HttpUploadServerInitializer()); + + Channel ch = b.bind().sync().channel(); + System.out.println("HTTP Upload Server at port " + port + '.'); + System.out.println("Open your browser and navigate to http://localhost:" + port + '/'); + + ch.closeFuture().sync(); + } finally { + b.shutdown(); + } + } + + public static void main(String[] args) throws Exception { + int port; + if (args.length > 0) { + port = Integer.parseInt(args[0]); + } else { + port = 8080; + } + if (args.length > 1) { + isSSL = true; + } + new HttpUploadServer(port).run(); + } +} diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java new file mode 100644 index 0000000000..bd129fa67f --- /dev/null +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java @@ -0,0 +1,427 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.example.http.upload; + +import static io.netty.buffer.Unpooled.copiedBuffer; +import static io.netty.handler.codec.http.HttpHeaders.Names.SET_COOKIE; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundMessageHandlerAdapter; +import io.netty.handler.codec.http.Cookie; +import io.netty.handler.codec.http.CookieDecoder; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpChunk; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.QueryStringDecoder; +import io.netty.handler.codec.http.ServerCookieEncoder; +import io.netty.handler.codec.http.multipart.Attribute; +import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; +import io.netty.handler.codec.http.multipart.DiskAttribute; +import io.netty.handler.codec.http.multipart.DiskFileUpload; +import io.netty.handler.codec.http.multipart.FileUpload; +import io.netty.handler.codec.http.multipart.HttpDataFactory; +import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; +import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException; +import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException; +import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.IncompatibleDataDecoderException; +import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException; +import io.netty.handler.codec.http.multipart.InterfaceHttpData; +import io.netty.handler.codec.http.multipart.InterfaceHttpData.HttpDataType; +import io.netty.logging.InternalLogger; +import io.netty.logging.InternalLoggerFactory; +import io.netty.util.CharsetUtil; + +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class HttpUploadServerHandler extends ChannelInboundMessageHandlerAdapter { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(HttpUploadServerHandler.class); + + private HttpRequest request; + + private boolean readingChunks; + + private final StringBuilder responseContent = new StringBuilder(); + + private static final HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); //Disk + // if + // size + // exceed + + private HttpPostRequestDecoder decoder; + static { + DiskFileUpload.deleteOnExitTemporaryFile = true; // should delete file + // on exit (in normal + // exit) + DiskFileUpload.baseDirectory = null; // system temp directory + DiskAttribute.deleteOnExitTemporaryFile = true; // should delete file on + // exit (in normal exit) + DiskAttribute.baseDirectory = null; // system temp directory + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + if (decoder != null) { + decoder.cleanFiles(); + } + }; + + @Override + public void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!readingChunks) { + // clean previous FileUpload if Any + if (decoder != null) { + decoder.cleanFiles(); + decoder = null; + } + + HttpRequest request = this.request = (HttpRequest) msg; + URI uri = new URI(request.getUri()); + if (!uri.getPath().startsWith("/form")) { + // Write Menu + writeMenu(ctx); + return; + } + responseContent.setLength(0); + responseContent.append("WELCOME TO THE WILD WILD WEB SERVER\r\n"); + responseContent.append("===================================\r\n"); + + responseContent.append("VERSION: " + request.getProtocolVersion().getText() + "\r\n"); + + responseContent.append("REQUEST_URI: " + request.getUri() + "\r\n\r\n"); + responseContent.append("\r\n\r\n"); + + // new method + List> headers = request.getHeaders(); + for (Entry entry : headers) { + responseContent.append("HEADER: " + entry.getKey() + "=" + entry.getValue() + "\r\n"); + } + responseContent.append("\r\n\r\n"); + + // new method + Set cookies; + String value = request.getHeader(HttpHeaders.Names.COOKIE); + if (value == null) { + cookies = Collections.emptySet(); + } else { + cookies = CookieDecoder.decode(value); + } + for (Cookie cookie : cookies) { + responseContent.append("COOKIE: " + cookie.toString() + "\r\n"); + } + responseContent.append("\r\n\r\n"); + + QueryStringDecoder decoderQuery = new QueryStringDecoder(request.getUri()); + Map> uriAttributes = decoderQuery.getParameters(); + for (String key : uriAttributes.keySet()) { + for (String valuen : uriAttributes.get(key)) { + responseContent.append("URI: " + key + "=" + valuen + "\r\n"); + } + } + responseContent.append("\r\n\r\n"); + + // if GET Method: should not try to create a HttpPostRequestDecoder + try { + decoder = new HttpPostRequestDecoder(factory, request); + } catch (ErrorDataDecoderException e1) { + e1.printStackTrace(); + responseContent.append(e1.getMessage()); + writeResponse(ctx.channel()); + ctx.channel().close(); + return; + } catch (IncompatibleDataDecoderException e1) { + // GET Method: should not try to create a HttpPostRequestDecoder + // So OK but stop here + responseContent.append(e1.getMessage()); + responseContent.append("\r\n\r\nEND OF GET CONTENT\r\n"); + writeResponse(ctx.channel()); + return; + } + + responseContent.append("Is Chunked: " + request.getTransferEncoding().isMultiple() + "\r\n"); + responseContent.append("IsMultipart: " + decoder.isMultipart() + "\r\n"); + if (request.getTransferEncoding().isMultiple()) { + // Chunk version + responseContent.append("Chunks: "); + readingChunks = true; + } else { + // Not chunk version + readHttpDataAllReceive(ctx.channel()); + responseContent.append("\r\n\r\nEND OF NOT CHUNKED CONTENT\r\n"); + writeResponse(ctx.channel()); + } + } else { + // New chunk is received + HttpChunk chunk = (HttpChunk) msg; + try { + decoder.offer(chunk); + } catch (ErrorDataDecoderException e1) { + e1.printStackTrace(); + responseContent.append(e1.getMessage()); + writeResponse(ctx.channel()); + ctx.channel().close(); + return; + } + responseContent.append("o"); + // example of reading chunk by chunk (minimize memory usage due to + // Factory) + readHttpDataChunkByChunk(ctx.channel()); + // example of reading only if at the end + if (chunk.isLast()) { + readHttpDataAllReceive(ctx.channel()); + writeResponse(ctx.channel()); + readingChunks = false; + } + } + } + + /** + * Example of reading all InterfaceHttpData from finished transfer + * + * @param channel + */ + private void readHttpDataAllReceive(Channel channel) { + List datas = null; + try { + datas = decoder.getBodyHttpDatas(); + } catch (NotEnoughDataDecoderException e1) { + // Should not be! + e1.printStackTrace(); + responseContent.append(e1.getMessage()); + writeResponse(channel); + channel.close(); + return; + } + for (InterfaceHttpData data : datas) { + writeHttpData(data); + } + responseContent.append("\r\n\r\nEND OF CONTENT AT FINAL END\r\n"); + } + + /** + * Example of reading request by chunk and getting values from chunk to chunk + * + * @param channel + */ + private void readHttpDataChunkByChunk(Channel channel) { + try { + while (decoder.hasNext()) { + InterfaceHttpData data = decoder.next(); + if (data != null) { + // new value + writeHttpData(data); + } + } + } catch (EndOfDataDecoderException e1) { + // end + responseContent.append("\r\n\r\nEND OF CONTENT CHUNK BY CHUNK\r\n\r\n"); + } + } + + private void writeHttpData(InterfaceHttpData data) { + if (data.getHttpDataType() == HttpDataType.Attribute) { + Attribute attribute = (Attribute) data; + String value; + try { + value = attribute.getValue(); + } catch (IOException e1) { + // Error while reading data from File, only print name and error + e1.printStackTrace(); + responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": " + + attribute.getName() + " Error while reading value: " + e1.getMessage() + "\r\n"); + return; + } + if (value.length() > 100) { + responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": " + + attribute.getName() + " data too long\r\n"); + } else { + responseContent.append("\r\nBODY Attribute: " + attribute.getHttpDataType().name() + ": " + + attribute.toString() + "\r\n"); + } + } else { + responseContent.append("\r\nBODY FileUpload: " + data.getHttpDataType().name() + ": " + data.toString() + + "\r\n"); + if (data.getHttpDataType() == HttpDataType.FileUpload) { + FileUpload fileUpload = (FileUpload) data; + if (fileUpload.isCompleted()) { + if (fileUpload.length() < 10000) { + responseContent.append("\tContent of file\r\n"); + try { + responseContent.append(((FileUpload) data).getString(((FileUpload) data).getCharset())); + } catch (IOException e1) { + // do nothing for the example + e1.printStackTrace(); + } + responseContent.append("\r\n"); + } else { + responseContent.append("\tFile too long to be printed out:" + fileUpload.length() + "\r\n"); + } + // fileUpload.isInMemory();// tells if the file is in Memory + // or on File + // fileUpload.renameTo(dest); // enable to move into another + // File dest + // decoder.removeFileUploadFromClean(fileUpload); //remove + // the File of to delete file + } else { + responseContent.append("\tFile to be continued but should not!\r\n"); + } + } + } + } + + private void writeResponse(Channel channel) { + // Convert the response content to a ChannelBuffer. + ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8); + responseContent.setLength(0); + + // Decide whether to close the connection or not. + boolean close = HttpHeaders.Values.CLOSE.equalsIgnoreCase(request.getHeader(HttpHeaders.Names.CONNECTION)) + || request.getProtocolVersion().equals(HttpVersion.HTTP_1_0) + && !HttpHeaders.Values.KEEP_ALIVE.equalsIgnoreCase(request.getHeader(HttpHeaders.Names.CONNECTION)); + + // Build the response object. + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + response.setContent(buf); + response.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8"); + + if (!close) { + // There's no need to add 'Content-Length' header + // if this is the last response. + response.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(buf.readableBytes())); + } + + Set cookies; + String value = request.getHeader(HttpHeaders.Names.COOKIE); + if (value == null) { + cookies = Collections.emptySet(); + } else { + cookies = CookieDecoder.decode(value); + } + if (!cookies.isEmpty()) { + // Reset the cookies if necessary. + for (Cookie cookie : cookies) { + response.addHeader(SET_COOKIE, ServerCookieEncoder.encode(cookie)); + } + } + // Write the response. + ChannelFuture future = channel.write(response); + // Close the connection after the write operation is done if necessary. + if (close) { + future.addListener(ChannelFutureListener.CLOSE); + } + } + + private void writeMenu(ChannelHandlerContext ctx) { + // print several HTML forms + // Convert the response content to a ChannelBuffer. + responseContent.setLength(0); + + // create Pseudo Menu + responseContent.append(""); + responseContent.append(""); + responseContent.append("Netty Test Form\r\n"); + responseContent.append("\r\n"); + responseContent.append(""); + + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append("
"); + responseContent.append("

Netty Test Form

"); + responseContent.append("Choose one FORM"); + responseContent.append("
\r\n"); + + // GET + responseContent.append("
GET FORM
"); + responseContent.append("
"); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append("
Fill with value:
Fill with value:
"); + responseContent + .append("
Fill with value:
"); + responseContent.append("
\r\n"); + responseContent.append("

"); + + // POST + responseContent.append("
POST FORM
"); + responseContent.append("
"); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append("
Fill with value:
Fill with value:
"); + responseContent + .append("
Fill with value:
"); + responseContent.append("
Fill with file (only file name will be transmitted):
" + + ""); + responseContent.append("
\r\n"); + responseContent.append("

"); + + // POST with enctype="multipart/form-data" + responseContent.append("
POST MULTIPART FORM
"); + responseContent.append("
"); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append(""); + responseContent.append("
Fill with value:
Fill with value:
"); + responseContent + .append("
Fill with value:
"); + responseContent.append("
Fill with file:
"); + responseContent.append("
\r\n"); + responseContent.append("

"); + + responseContent.append(""); + responseContent.append(""); + + ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8); + // Build the response object. + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + response.setContent(buf); + response.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=UTF-8"); + response.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(buf.readableBytes())); + // Write the response. + ctx.channel().write(response); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + logger.error(responseContent.toString(), cause); + ctx.channel().close(); + } +} diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadServerInitializer.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerInitializer.java new file mode 100644 index 0000000000..1ef05adbbe --- /dev/null +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerInitializer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project 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. + */ +package io.netty.example.http.upload; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.example.securechat.SecureChatSslContextFactory; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.ssl.SslHandler; + +import javax.net.ssl.SSLEngine; + +public class HttpUploadServerInitializer extends ChannelInitializer { + @Override + public void initChannel(SocketChannel ch) throws Exception { + // Create a default pipeline implementation. + ChannelPipeline pipeline = ch.pipeline(); + + if (HttpUploadServer.isSSL) { + SSLEngine engine = SecureChatSslContextFactory.getServerContext().createSSLEngine(); + engine.setUseClientMode(false); + pipeline.addLast("ssl", new SslHandler(engine)); + } + + pipeline.addLast("decoder", new HttpRequestDecoder()); + pipeline.addLast("encoder", new HttpResponseEncoder()); + + // Remove the following line if you don't want automatic content + // compression. + pipeline.addLast("deflater", new HttpContentCompressor()); + + pipeline.addLast("handler", new HttpUploadServerHandler()); + + return; + } +}