diff --git a/.gitignore b/.gitignore index 108242bdaf..bb41309de3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /*.iml /*.ipr /*.iws +/.metadata diff --git a/src/main/java/org/jboss/netty/example/http/upload/HttpUploadClient.java b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadClient.java new file mode 100644 index 0000000000..c0d1809d17 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadClient.java @@ -0,0 +1,985 @@ +/* + * Copyright 2011 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 org.jboss.netty.example.http.upload; + +import java.io.File; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.Executors; + +import org.jboss.netty.bootstrap.ClientBootstrap; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; +import org.jboss.netty.handler.codec.http.CookieEncoder; +import org.jboss.netty.handler.codec.http.DefaultHttpDataFactory; +import org.jboss.netty.handler.codec.http.DefaultHttpRequest; +import org.jboss.netty.handler.codec.http.DiskAttribute; +import org.jboss.netty.handler.codec.http.DiskFileUpload; +import org.jboss.netty.handler.codec.http.HttpDataFactory; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpPostRequestEncoder; +import org.jboss.netty.handler.codec.http.HttpPostRequestEncoder.ErrorDataEncoderException; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpVersion; +import org.jboss.netty.handler.codec.http.InterfaceHttpData; +import org.jboss.netty.handler.codec.http.QueryStringEncoder; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +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() { + 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. + ClientBootstrap bootstrap = new ClientBootstrap( + new NioClientSocketChannelFactory( + Executors.newCachedThreadPool(), + Executors.newCachedThreadPool())); + + // Set up the event pipeline factory. + bootstrap.setPipelineFactory(new HttpUploadClientPipelineFactory(ssl)); + + // setup the factory: here using a mixed memory/disk based on size threshold + HttpDataFactory factory = new DefaultHttpDataFactory( + DefaultHttpDataFactory.MINSIZE); // Disk if size exceed MINSIZE + 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(bootstrap, host, port, get, uriSimple); + if (headers == null) { + factory.cleanAllHttpDatas(); + return; + } + // Simple Post form: factory used for big attributes + List bodylist = + formpost(bootstrap, host, port, uriSimple, file, factory, headers); + if (bodylist == null) { + factory.cleanAllHttpDatas(); + return; + } + // Multipart Post form: factory used + formpostmultipart(bootstrap, host, port, uriFile, factory, headers, bodylist); + + // Shut down executor threads to exit. + bootstrap.releaseExternalResources(); + // 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(ClientBootstrap bootstrap, String host, int port, String get, + URI uriSimple) { + // XXX /formget + // No use of HttpPostRequestEncoder since not a POST + // Start the connection attempt. + ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port)); + // Wait until the connection attempt succeeds or fails. + Channel channel = future.awaitUninterruptibly().getChannel(); + if (!future.isSuccess()) { + future.getCause().printStackTrace(); + bootstrap.releaseExternalResources(); + return null; + } + + // 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()); + bootstrap.releaseExternalResources(); + 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"); + + CookieEncoder httpCookieEncoder = new CookieEncoder(false); + httpCookieEncoder.addCookie("my-cookie", "foo"); + httpCookieEncoder.addCookie("another-cookie", "bar"); + request.setHeader(HttpHeaders.Names.COOKIE, httpCookieEncoder.encode()); + + List> headers = request.getHeaders(); + // send request + channel.write(request); + + // Wait for the server to close the connection. + channel.getCloseFuture().awaitUninterruptibly(); + + 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(ClientBootstrap bootstrap, + String host, int port, + URI uriSimple, File file, HttpDataFactory factory, + List> headers) { + // XXX /formpost + // Start the connection attempt. + ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port)); + // Wait until the connection attempt succeeds or fails. + Channel channel = future.awaitUninterruptibly().getChannel(); + if (!future.isSuccess()) { + future.getCause().printStackTrace(); + bootstrap.releaseExternalResources(); + return null; + } + + // 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.getCloseFuture().awaitUninterruptibly(); + return bodylist; + } + + /** + * Multipart example + */ + private static void formpostmultipart(ClientBootstrap bootstrap, String host, int port, + URI uriFile, HttpDataFactory factory, + List> headers, List bodylist) { + // XXX /formpostmultipart + // Start the connection attempt. + ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port)); + // Wait until the connection attempt succeeds or fails. + Channel channel = future.awaitUninterruptibly().getChannel(); + if (!future.isSuccess()) { + future.getCause().printStackTrace(); + bootstrap.releaseExternalResources(); + return; + } + + // 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.getCloseFuture().awaitUninterruptibly(); + } + + public static void main(String[] args) { + if (args.length != 2) { + logger.error( + "Usage: " + HttpUploadClient.class.getSimpleName() + + " baseURI filePath"); + return; + } + + String baseUri = args[0]; + String filePath = args[1]; + + new HttpUploadClient(baseUri, filePath).run(); + } + + // use to simulate a big TEXTAREA field in a form + private static final String textArea = + "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/src/main/java/org/jboss/netty/example/http/upload/HttpUploadClientHandler.java b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadClientHandler.java new file mode 100644 index 0000000000..21b281e02a --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadClientHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright 2011 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 org.jboss.netty.example.http.upload; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.HttpChunk; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; +import org.jboss.netty.util.CharsetUtil; + +public class HttpUploadClientHandler extends SimpleChannelUpstreamHandler { + + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(HttpUploadClientHandler.class); + + private volatile boolean readingChunks; + + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + if (!readingChunks) { + HttpResponse response = (HttpResponse) e.getMessage(); + + 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.isChunked()) { + readingChunks = true; + logger.info("CHUNKED CONTENT {"); + } else { + ChannelBuffer 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) e.getMessage(); + if (chunk.isLast()) { + readingChunks = false; + logger.info("} END OF CHUNKED CONTENT"); + } else { + logger.info(chunk.getContent().toString(CharsetUtil.UTF_8)); + } + } + } + + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) + throws Exception { + e.getCause().printStackTrace(); + e.getChannel().close(); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/upload/HttpUploadClientPipelineFactory.java b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadClientPipelineFactory.java new file mode 100644 index 0000000000..6bc743cd58 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadClientPipelineFactory.java @@ -0,0 +1,61 @@ +/* + * Copyright 2011 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 org.jboss.netty.example.http.upload; + +import static org.jboss.netty.channel.Channels.*; + +import javax.net.ssl.SSLEngine; + +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.example.securechat.SecureChatSslContextFactory; +import org.jboss.netty.handler.codec.http.HttpClientCodec; +import org.jboss.netty.handler.codec.http.HttpContentDecompressor; +import org.jboss.netty.handler.ssl.SslHandler; +import org.jboss.netty.handler.stream.ChunkedWriteHandler; + +public class HttpUploadClientPipelineFactory implements ChannelPipelineFactory { + private final boolean ssl; + + public HttpUploadClientPipelineFactory(boolean ssl) { + this.ssl = ssl; + } + + public ChannelPipeline getPipeline() throws Exception { + // Create a default pipeline implementation. + ChannelPipeline pipeline = pipeline(); + + // Enable HTTPS if necessary. + 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()); + return pipeline; + } +} diff --git a/src/main/java/org/jboss/netty/example/http/upload/HttpUploadServer.java b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadServer.java new file mode 100644 index 0000000000..b6bb88614f --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadServer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2011 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 org.jboss.netty.example.http.upload; + +import java.net.InetSocketAddress; +import java.util.concurrent.Executors; + +import org.jboss.netty.bootstrap.ServerBootstrap; +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; + +public class HttpUploadServer { + + private final int port; + + public HttpUploadServer(int port) { + this.port = port; + } + + public void run() { + // Configure the server. + ServerBootstrap bootstrap = new ServerBootstrap( + new NioServerSocketChannelFactory( + Executors.newCachedThreadPool(), + Executors.newCachedThreadPool())); + + // Set up the event pipeline factory. + bootstrap.setPipelineFactory(new HttpUploadServerPipelineFactory()); + + // Bind and start to accept incoming connections. + bootstrap.bind(new InetSocketAddress(port)); + } + + public static void main(String[] args) { + int port; + if (args.length > 0) { + port = Integer.parseInt(args[0]); + } else { + port = 8080; + } + new HttpUploadServer(port).run(); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/upload/HttpUploadServerHandler.java b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadServerHandler.java new file mode 100644 index 0000000000..526229a0f0 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadServerHandler.java @@ -0,0 +1,488 @@ +/* + * Copyright 2011 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 org.jboss.netty.example.http.upload; + +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; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.Attribute; +import org.jboss.netty.handler.codec.http.Cookie; +import org.jboss.netty.handler.codec.http.CookieDecoder; +import org.jboss.netty.handler.codec.http.CookieEncoder; +import org.jboss.netty.handler.codec.http.DefaultHttpDataFactory; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.DiskAttribute; +import org.jboss.netty.handler.codec.http.DiskFileUpload; +import org.jboss.netty.handler.codec.http.FileUpload; +import org.jboss.netty.handler.codec.http.HttpChunk; +import org.jboss.netty.handler.codec.http.HttpDataFactory; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpPostRequestDecoder; +import org.jboss.netty.handler.codec.http.HttpPostRequestDecoder.EndOfDataDecoderException; +import org.jboss.netty.handler.codec.http.HttpPostRequestDecoder.ErrorDataDecoderException; +import org.jboss.netty.handler.codec.http.HttpPostRequestDecoder.IncompatibleDataDecoderException; +import org.jboss.netty.handler.codec.http.HttpPostRequestDecoder.NotEnoughDataDecoderException; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.jboss.netty.handler.codec.http.HttpVersion; +import org.jboss.netty.handler.codec.http.InterfaceHttpData; +import org.jboss.netty.handler.codec.http.InterfaceHttpData.HttpDataType; +import org.jboss.netty.handler.codec.http.QueryStringDecoder; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; +import org.jboss.netty.util.CharsetUtil; + +public class HttpUploadServerHandler extends SimpleChannelUpstreamHandler { + + 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 MINSIZE + + 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 + } + + public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) + throws Exception { + if (decoder != null) { + decoder.cleanFiles(); + } + } + + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + if (!readingChunks) { + // clean previous FileUpload if Any + if (decoder != null) { + decoder.cleanFiles(); + decoder = null; + } + + HttpRequest request = this.request = (HttpRequest) e.getMessage(); + URI uri = new URI(request.getUri()); + if (!uri.getPath().startsWith("/form")) { + // Write Menu + writeMenu(e); + 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 { + CookieDecoder decoder = new CookieDecoder(); + cookies = decoder.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(e.getChannel()); + Channels.close(e.getChannel()); + 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(e.getChannel()); + return; + } + + responseContent.append("Is Chunked: " + request.isChunked() + + "\r\n"); + responseContent.append("IsMultipart: " + decoder.isMultipart() + + "\r\n"); + if (request.isChunked()) { + // Chunk version + responseContent.append("Chunks: "); + readingChunks = true; + } else { + // Not chunk version + readHttpDataAllReceive(e.getChannel()); + responseContent + .append("\r\n\r\nEND OF NOT CHUNKED CONTENT\r\n"); + writeResponse(e.getChannel()); + } + } else { + // New chunk is received + HttpChunk chunk = (HttpChunk) e.getMessage(); + try { + decoder.offer(chunk); + } catch (ErrorDataDecoderException e1) { + e1.printStackTrace(); + responseContent.append(e1.getMessage()); + writeResponse(e.getChannel()); + Channels.close(e.getChannel()); + return; + } + responseContent.append("o"); + // example of reading chunk by chunk (minimize memory usage due to Factory) + readHttpDataChunkByChunk(e.getChannel()); + // example of reading only if at the end + if (chunk.isLast()) { + readHttpDataAllReceive(e.getChannel()); + writeResponse(e.getChannel()); + 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); + Channels.close(channel); + 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. + ChannelBuffer buf = ChannelBuffers.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 { + CookieDecoder decoder = new CookieDecoder(); + cookies = decoder.decode(value); + } + if (!cookies.isEmpty()) { + // Reset the cookies if necessary. + CookieEncoder cookieEncoder = new CookieEncoder(true); + for (Cookie cookie: cookies) { + cookieEncoder.addCookie(cookie); + } + response.addHeader(HttpHeaders.Names.SET_COOKIE, cookieEncoder + .encode()); + } + // 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(MessageEvent e) { + // 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(""); + + ChannelBuffer buf = ChannelBuffers.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. + e.getChannel().write(response); + } + + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) + throws Exception { + logger.error(responseContent.toString(), e.getCause()); + e.getChannel().close(); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/upload/HttpUploadServerPipelineFactory.java b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadServerPipelineFactory.java new file mode 100644 index 0000000000..e81df7ee03 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/upload/HttpUploadServerPipelineFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2011 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 org.jboss.netty.example.http.upload; + +import static org.jboss.netty.channel.Channels.pipeline; + +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.handler.codec.http.HttpContentCompressor; +import org.jboss.netty.handler.codec.http.HttpRequestDecoder; +import org.jboss.netty.handler.codec.http.HttpResponseEncoder; + +public class HttpUploadServerPipelineFactory implements ChannelPipelineFactory { + public ChannelPipeline getPipeline() throws Exception { + // Create a default pipeline implementation. + ChannelPipeline pipeline = pipeline(); + + // Uncomment the following line if you want HTTPS + //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 pipeline; + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/AbstractDiskHttpData.java b/src/main/java/org/jboss/netty/handler/codec/http/AbstractDiskHttpData.java new file mode 100644 index 0000000000..4d54c741d7 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/AbstractDiskHttpData.java @@ -0,0 +1,347 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +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; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * 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; + } + + @Override + public void setContent(ChannelBuffer 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.toByteBuffer(); + int written = 0; + while (written < size) { + written += localfileChannel.write(byteBuffer); + localfileChannel.force(false); + } + buffer.readerIndex(buffer.readerIndex() + written); + localfileChannel.close(); + completed = true; + } + + @Override + public void addContent(ChannelBuffer 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.toByteBuffer(); + 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); + fileChannel.force(false); + } + 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.close(); + fileChannel = null; + completed = true; + } else { + if (buffer == null) { + throw new NullPointerException("buffer"); + } + } + } + + @Override + public void setContent(File file) throws IOException { + if (this.file != null) { + delete(); + } + this.file = file; + size = file.length(); + isRenamed = true; + completed = true; + } + + @Override + 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); + localfileChannel.force(false); + read = inputStream.read(bytes); + } + size = written; + if (definedSize > 0 && definedSize < size) { + file.delete(); + file = null; + throw new IOException("Out of size: " + size + " > " + definedSize); + } + isRenamed = true; + completed = true; + } + + @Override + public void delete() { + if (! isRenamed) { + if (file != null) { + file.delete(); + } + } + } + + @Override + public byte[] get() throws IOException { + if (file == null) { + return new byte[0]; + } + return readFrom(file); + } + + @Override + public ChannelBuffer getChannelBuffer() throws IOException { + if (file == null) { + return ChannelBuffers.EMPTY_BUFFER; + } + byte[] array = readFrom(file); + return ChannelBuffers.wrappedBuffer(array); + } + + @Override + public ChannelBuffer getChunk(int length) throws IOException { + if (file == null || length == 0) { + return ChannelBuffers.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 ChannelBuffers.EMPTY_BUFFER; + } + byteBuffer.flip(); + ChannelBuffer buffer = ChannelBuffers.wrappedBuffer(byteBuffer); + buffer.readerIndex(0); + buffer.writerIndex(read); + return buffer; + } + + @Override + public String getString() throws IOException { + return getString(HttpCodecUtil.DEFAULT_CHARSET); + } + + @Override + public String getString(Charset encoding) throws IOException { + if (file == null) { + return ""; + } + if (encoding == null) { + byte[] array = readFrom(file); + return new String(array, HttpCodecUtil.DEFAULT_CHARSET); + } + byte[] array = readFrom(file); + return new String(array, encoding); + } + + @Override + public boolean isInMemory() { + return false; + } + + @Override + 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(); + long destsize = in.transferTo(0, size, out); + if (destsize == 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 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; + } + + @Override + public File getFile() throws IOException { + return file; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/AbstractHttpData.java b/src/main/java/org/jboss/netty/handler/codec/http/AbstractHttpData.java new file mode 100644 index 0000000000..4e3e043feb --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/AbstractHttpData.java @@ -0,0 +1,97 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +import java.nio.charset.Charset; + +/** + * Abstract HttpData implementation + */ +public abstract class AbstractHttpData implements HttpData { + + protected final String name; + protected long definedSize; + protected long size; + protected Charset charset = HttpCodecUtil.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; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isCompleted() { + return completed; + } + + @Override + public Charset getCharset() { + return charset; + } + + @Override + public void setCharset(Charset charset) { + if (charset == null) { + throw new NullPointerException("charset"); + } + this.charset = charset; + } + + @Override + public long length() { + return size; + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/AbstractMemoryHttpData.java b/src/main/java/org/jboss/netty/handler/codec/http/AbstractMemoryHttpData.java new file mode 100644 index 0000000000..46f7e5e08b --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/AbstractMemoryHttpData.java @@ -0,0 +1,226 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +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; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * Abstract Memory HttpData implementation + */ +public abstract class AbstractMemoryHttpData extends AbstractHttpData { + + private ChannelBuffer channelBuffer; + private int chunkPosition; + protected boolean isRenamed; + + public AbstractMemoryHttpData(String name, Charset charset, long size) { + super(name, charset, size); + } + + @Override + public void setContent(ChannelBuffer 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); + } + channelBuffer = buffer; + size = localsize; + completed = true; + } + + @Override + public void setContent(InputStream inputStream) throws IOException { + if (inputStream == null) { + throw new NullPointerException("inputStream"); + } + ChannelBuffer buffer = ChannelBuffers.dynamicBuffer(); + 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); + } + channelBuffer = buffer; + completed = true; + } + + @Override + public void addContent(ChannelBuffer 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 (channelBuffer == null) { + channelBuffer = buffer; + } else { + channelBuffer = ChannelBuffers.wrappedBuffer( + channelBuffer, buffer); + } + } + if (last) { + completed = true; + } else { + if (buffer == null) { + throw new NullPointerException("buffer"); + } + } + } + + @Override + 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(); + byteBuffer.flip(); + channelBuffer = ChannelBuffers.wrappedBuffer(byteBuffer); + size = newsize; + completed = true; + } + + @Override + public void delete() { + // nothing to do + } + + @Override + public byte[] get() { + if (channelBuffer == null) { + return new byte[0]; + } + byte[] array = new byte[channelBuffer.readableBytes()]; + channelBuffer.getBytes(channelBuffer.readerIndex(), array); + return array; + } + + @Override + public String getString() { + return getString(HttpCodecUtil.DEFAULT_CHARSET); + } + + @Override + public String getString(Charset encoding) { + if (channelBuffer == null) { + return ""; + } + if (encoding == null) { + return getString(HttpCodecUtil.DEFAULT_CHARSET); + } + return channelBuffer.toString(encoding); + } + + /** + * Utility to go from a In Memory FileUpload + * to a Disk (or another implementation) FileUpload + * @return the attached ChannelBuffer containing the actual bytes + */ + @Override + public ChannelBuffer getChannelBuffer() { + return channelBuffer; + } + + @Override + public ChannelBuffer getChunk(int length) throws IOException { + if (channelBuffer == null || length == 0 || channelBuffer.readableBytes() == 0) { + chunkPosition = 0; + return ChannelBuffers.EMPTY_BUFFER; + } + int sizeLeft = channelBuffer.readableBytes() - chunkPosition; + if (sizeLeft == 0) { + chunkPosition = 0; + return ChannelBuffers.EMPTY_BUFFER; + } + int sliceLength = length; + if (sizeLeft < length) { + sliceLength = sizeLeft; + } + ChannelBuffer chunk = channelBuffer.slice(chunkPosition, sliceLength); + chunkPosition += sliceLength; + return chunk; + } + + @Override + public boolean isInMemory() { + return true; + } + + @Override + public boolean renameTo(File dest) throws IOException { + if (dest == null) { + throw new NullPointerException("dest"); + } + if (channelBuffer == null) { + // empty file + dest.createNewFile(); + isRenamed = true; + return true; + } + int length = channelBuffer.readableBytes(); + FileOutputStream outputStream = new FileOutputStream(dest); + FileChannel fileChannel = outputStream.getChannel(); + ByteBuffer byteBuffer = channelBuffer.toByteBuffer(); + int written = 0; + while (written < length) { + written += fileChannel.write(byteBuffer); + fileChannel.force(false); + } + fileChannel.close(); + isRenamed = true; + return written == length; + } + + @Override + public File getFile() throws IOException { + throw new IOException("Not represented by a file"); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/Attribute.java b/src/main/java/org/jboss/netty/handler/codec/http/Attribute.java new file mode 100644 index 0000000000..ab6a5b9327 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/Attribute.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +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/src/main/java/org/jboss/netty/handler/codec/http/DefaultHttpDataFactory.java b/src/main/java/org/jboss/netty/handler/codec/http/DefaultHttpDataFactory.java new file mode 100644 index 0000000000..50d352f617 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/DefaultHttpDataFactory.java @@ -0,0 +1,188 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 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 boolean useDisk; + + private 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; + this.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; + } + + @Override + 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); + } + + @Override + 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); + } + } + + @Override + 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); + } + + @Override + public void removeHttpDataFromClean(HttpRequest request, InterfaceHttpData data) { + if (data instanceof HttpData) { + List fileToDelete = getList(request); + fileToDelete.remove(data); + } + } + + @Override + public void cleanRequestHttpDatas(HttpRequest request) { + List fileToDelete = requestFileDeleteMap.remove(request); + if (fileToDelete != null) { + for (HttpData data: fileToDelete) { + data.delete(); + } + fileToDelete.clear(); + } + } + + @Override + 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/src/main/java/org/jboss/netty/handler/codec/http/DiskAttribute.java b/src/main/java/org/jboss/netty/handler/codec/http/DiskAttribute.java new file mode 100644 index 0000000000..8ae34fd092 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/DiskAttribute.java @@ -0,0 +1,147 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +import java.io.IOException; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * 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, HttpCodecUtil.DEFAULT_CHARSET, 0); + } + /** + * + * @param name + * @param value + * @throws NullPointerException + * @throws IllegalArgumentException + * @throws IOException + */ + public DiskAttribute(String name, String value) throws IOException { + super(name, HttpCodecUtil.DEFAULT_CHARSET, 0); // Attribute have no default size + setValue(value); + } + + @Override + public HttpDataType getHttpDataType() { + return HttpDataType.Attribute; + } + + @Override + public String getValue() throws IOException { + byte [] bytes = get(); + return new String(bytes, charset); + } + + @Override + public void setValue(String value) throws IOException { + if (value == null) { + throw new NullPointerException("value"); + } + byte [] bytes = value.getBytes(charset); + ChannelBuffer buffer = ChannelBuffers.wrappedBuffer(bytes); + if (definedSize > 0) { + definedSize = buffer.readableBytes(); + } + setContent(buffer); + } + + @Override + public void addContent(ChannelBuffer 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()); + } + + @Override + 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/src/main/java/org/jboss/netty/handler/codec/http/DiskFileUpload.java b/src/main/java/org/jboss/netty/handler/codec/http/DiskFileUpload.java new file mode 100644 index 0000000000..2141325e9c --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/DiskFileUpload.java @@ -0,0 +1,160 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +import java.io.File; +import java.nio.charset.Charset; + +/** + * 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); + } + + @Override + public HttpDataType getHttpDataType() { + return HttpDataType.FileUpload; + } + + @Override + public String getFilename() { + return filename; + } + + @Override + 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()); + } + + @Override + 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; + } + + @Override + public void setContentType(String contentType) { + if (contentType == null) { + throw new NullPointerException("contentType"); + } + this.contentType = contentType; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public String getContentTransferEncoding() { + return contentTransferEncoding; + } + + @Override + 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/src/main/java/org/jboss/netty/handler/codec/http/FileUpload.java b/src/main/java/org/jboss/netty/handler/codec/http/FileUpload.java new file mode 100644 index 0000000000..7bbd32afd8 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/FileUpload.java @@ -0,0 +1,60 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +/** + * 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(); +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpData.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpData.java new file mode 100644 index 0000000000..c7c145aca1 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpData.java @@ -0,0 +1,181 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +import org.jboss.netty.buffer.ChannelBuffer; + +/** + * 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(ChannelBuffer 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(ChannelBuffer 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 ChannelBuffer + * + * @return the content of the file item as a ChannelBuffer + * @throws IOException + */ + ChannelBuffer getChannelBuffer() 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 + */ + ChannelBuffer 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; + +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpDataFactory.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpDataFactory.java new file mode 100644 index 0000000000..7dac637bd3 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpDataFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +import java.nio.charset.Charset; + +import org.jboss.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(); +} \ No newline at end of file diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpHeaders.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpHeaders.java index 7b277ec5a8..8e900e07ca 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/HttpHeaders.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpHeaders.java @@ -306,6 +306,10 @@ public class HttpHeaders { * @apiviz.stereotype static */ public static final class Values { + /** + * {@code "application/x-www-form-urlencoded"} + */ + public static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded"; /** * {@code "base64"} */ @@ -314,6 +318,10 @@ public class HttpHeaders { * {@code "binary"} */ public static final String BINARY = "binary"; + /** + * {@code "boundary"} + */ + static final String BOUNDARY = "boundary"; /** * {@code "bytes"} */ @@ -366,6 +374,10 @@ public class HttpHeaders { * {@code "min-fresh"} */ public static final String MIN_FRESH = "min-fresh"; + /** + * {@code "multipart/form-data"} + */ + static final String MULTIPART_FORM_DATA = "multipart/form-data"; /** * {@code "must-revalidate"} */ diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpPostBodyUtil.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpPostBodyUtil.java new file mode 100644 index 0000000000..6168763924 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpPostBodyUtil.java @@ -0,0 +1,181 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +import java.nio.charset.Charset; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.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() { + } + + //Some commons methods between HttpPostRequestDecoder and HttpMessageDecoder + /** + * Skip control Characters + * @param buffer + */ + static void skipControlCharacters(ChannelBuffer buffer) { + for (;;) { + char c = (char) buffer.readUnsignedByte(); + if (!Character.isISOControl(c) && !Character.isWhitespace(c)) { + buffer.readerIndex(buffer.readerIndex() - 1); + break; + } + } + } + + /** + * 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/src/main/java/org/jboss/netty/handler/codec/http/HttpPostRequestDecoder.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpPostRequestDecoder.java new file mode 100644 index 0000000000..8f1ea9e1df --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpPostRequestDecoder.java @@ -0,0 +1,1499 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +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 org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.handler.codec.http.HttpPostBodyUtil.TransferEncodingMechanism; + +/** + * 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 ChannelBuffer 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, HttpCodecUtil.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, HttpCodecUtil.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.isChunked()) { + 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 { + ChannelBuffer chunked = chunk.getContent(); + if (undecodedChunk == null) { + undecodedChunk = chunked; + } else { + //undecodedChunk = ChannelBuffers.wrappedBuffer(undecodedChunk, chunk.getContent()); + // less memory usage + undecodedChunk = ChannelBuffers.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 parseBodyAttributes() 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 == HttpCodecUtil.CR) { + if (undecodedChunk.readable()) { + read = (char) undecodedChunk.readUnsignedByte(); + currentpos++; + if (read == HttpCodecUtil.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 == HttpCodecUtil.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(ChannelBuffers.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(ChannelBuffer buffer) throws ErrorDataDecoderException, IOException { + currentAttribute.addContent(buffer, true); + String value = decodeAttribute( + currentAttribute.getChannelBuffer().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); + } + } + + /** + * 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."); + } + } + + /** + * 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(); + HttpPostBodyUtil.skipControlCharacters(undecodedChunk); + skipOneLine(); + String newline; + try { + newline = readLine(); + } 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()) { + HttpPostBodyUtil.skipControlCharacters(undecodedChunk); + String newline; + try { + 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 readLine() throws NotEnoughDataDecoderException { + int readerIndex = undecodedChunk.readerIndex(); + try { + StringBuilder sb = new StringBuilder(64); + while (undecodedChunk.readable()) { + byte nextByte = undecodedChunk.readByte(); + if (nextByte == HttpCodecUtil.CR) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpCodecUtil.LF) { + return sb.toString(); + } + } else if (nextByte == HttpCodecUtil.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 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 { + 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 == HttpCodecUtil.CR) { + if (undecodedChunk.readable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpCodecUtil.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } + } + } else if (nextByte == HttpCodecUtil.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 == HttpCodecUtil.CR) { + if (undecodedChunk.readable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpCodecUtil.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } + } + } else if (nextByte == HttpCodecUtil.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 1; + } else { + // save last valid position + lastPosition = undecodedChunk.readerIndex(); + } + } + } + ChannelBuffer 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 loadFieldMultipart(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 == HttpCodecUtil.CR) { + if (undecodedChunk.readable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpCodecUtil.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } + } + } else if (nextByte == HttpCodecUtil.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 1; + } else { + lastPosition = undecodedChunk.readerIndex(); + } + } + } else { + // continue until end of line + if (nextByte == HttpCodecUtil.CR) { + if (undecodedChunk.readable()) { + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpCodecUtil.LF) { + newLine = true; + index = 0; + lastPosition = undecodedChunk.readerIndex() - 2; + } + } + } else if (nextByte == HttpCodecUtil.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); + } + } + + /** + * Clean the String from any unallowed character + * @return the cleaned String + */ + private 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 == HttpCodecUtil.COLON) { + sb.append(HttpCodecUtil.SP); + } else if (nextChar == HttpCodecUtil.COMMA) { + sb.append(HttpCodecUtil.SP); + } else if (nextChar == HttpCodecUtil.EQUALS) { + sb.append(HttpCodecUtil.SP); + } else if (nextChar == HttpCodecUtil.SEMICOLON) { + sb.append(HttpCodecUtil.SP); + } else if (nextChar == HttpCodecUtil.HT) { + sb.append(HttpCodecUtil.SP); + } else if (nextChar == HttpCodecUtil.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 == HttpCodecUtil.CR) { + if (!undecodedChunk.readable()) { + undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1); + return false; + } + nextByte = undecodedChunk.readByte(); + if (nextByte == HttpCodecUtil.LF) { + return true; + } + undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 2); + return false; + } else if (nextByte == HttpCodecUtil.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 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 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/src/main/java/org/jboss/netty/handler/codec/http/HttpPostRequestEncoder.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpPostRequestEncoder.java new file mode 100644 index 0000000000..e7e3b87b55 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpPostRequestEncoder.java @@ -0,0 +1,985 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +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 org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.handler.stream.ChunkedInput; + +/** + * This encoder will help to encode Request for a FORM as POST. + */ +public class HttpPostRequestEncoder implements ChunkedInput { + /** + * 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 List bodyListDatas; + /** + * The final Multipart List of InterfaceHttpData including encoding + */ + private 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, HttpCodecUtil.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, HttpCodecUtil.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 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) { + 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.addHeader(HttpHeaders.Names.TRANSFER_ENCODING, + HttpHeaders.Values.CHUNKED); + request.setContent(ChannelBuffers.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 ChannelBuffer currently used by the encoder + */ + private ChannelBuffer 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 ChannelBuffer to send as a HttpChunk and modifying currentBuffer + * accordingly + */ + private ChannelBuffer fillChannelBuffer() { + int length = currentBuffer.readableBytes(); + if (length > HttpPostBodyUtil.chunkSize) { + ChannelBuffer slice = + currentBuffer.slice(currentBuffer.readerIndex(), HttpPostBodyUtil.chunkSize); + currentBuffer.skipBytes(HttpPostBodyUtil.chunkSize); + return slice; + } else { + // to continue + ChannelBuffer 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; + } + ChannelBuffer 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 = ChannelBuffers.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 = ChannelBuffers.wrappedBuffer(currentBuffer, + buffer); + } + if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) { + currentData = null; + return null; + } + buffer = fillChannelBuffer(); + 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; + ChannelBuffer buffer; + if (isKey) { + // get name + String key = currentData.getName(); + buffer = ChannelBuffers.wrappedBuffer(key.getBytes()); + isKey = false; + if (currentBuffer == null) { + currentBuffer = ChannelBuffers.wrappedBuffer( + buffer, ChannelBuffers.wrappedBuffer("=".getBytes())); + //continue + size -= buffer.readableBytes() + 1; + } else { + currentBuffer = ChannelBuffers.wrappedBuffer(currentBuffer, + buffer, ChannelBuffers.wrappedBuffer("=".getBytes())); + //continue + size -= buffer.readableBytes() + 1; + } + if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) { + buffer = fillChannelBuffer(); + return new DefaultHttpChunk(buffer); + } + } + try { + buffer = ((Attribute) currentData).getChunk(size); + } catch (IOException e) { + throw new ErrorDataEncoderException(e); + } + ChannelBuffer delimiter = null; + if (buffer.readableBytes() < size) { + // delimiter + isKey = true; + delimiter = iterator.hasNext() ? + ChannelBuffers.wrappedBuffer("&".getBytes()) : + null; + } + if (buffer.capacity() == 0) { + // end for current InterfaceHttpData, need potentially more data + currentData = null; + if (currentBuffer == null) { + currentBuffer = delimiter; + } else { + if (delimiter != null) { + currentBuffer = ChannelBuffers.wrappedBuffer(currentBuffer, + delimiter); + } + } + if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) { + buffer = fillChannelBuffer(); + return new DefaultHttpChunk(buffer); + } + return null; + } + if (currentBuffer == null) { + if (delimiter != null) { + currentBuffer = ChannelBuffers.wrappedBuffer(buffer, + delimiter); + } else { + currentBuffer = buffer; + } + } else { + if (delimiter != null) { + currentBuffer = ChannelBuffers.wrappedBuffer(currentBuffer, + buffer, delimiter); + } else { + currentBuffer = ChannelBuffers.wrappedBuffer(currentBuffer, + buffer); + } + } + if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) { + // end for current InterfaceHttpData, need more data + currentData = null; + isKey = true; + return null; + } + buffer = fillChannelBuffer(); + // size = 0 + return new DefaultHttpChunk(buffer); + } + + @Override + 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 + */ + @Override + public HttpChunk nextChunk() throws ErrorDataEncoderException { + if (isLastChunk) { + isLastChunkSent = true; + return new DefaultHttpChunk(ChannelBuffers.EMPTY_BUFFER); + } + ChannelBuffer 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 = fillChannelBuffer(); + 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(ChannelBuffers.EMPTY_BUFFER); + } + //Previous LastChunk with no more data + buffer = currentBuffer; + currentBuffer = null; + return new DefaultHttpChunk(buffer); + } + + @Override + public boolean isEndOfInput() throws Exception { + return isLastChunkSent; + } + + + public boolean hasNextChunk() 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/src/main/java/org/jboss/netty/handler/codec/http/InterfaceHttpData.java b/src/main/java/org/jboss/netty/handler/codec/http/InterfaceHttpData.java new file mode 100644 index 0000000000..53a55129ef --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/InterfaceHttpData.java @@ -0,0 +1,36 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +/** + * 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/src/main/java/org/jboss/netty/handler/codec/http/InternalAttribute.java b/src/main/java/org/jboss/netty/handler/codec/http/InternalAttribute.java new file mode 100644 index 0000000000..0d5b9f4547 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/InternalAttribute.java @@ -0,0 +1,105 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +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(); + + @Override + 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()); + } + + @Override + 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(); + } + + @Override + public String getName() { + return "InternalAttribute"; + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/MemoryAttribute.java b/src/main/java/org/jboss/netty/handler/codec/http/MemoryAttribute.java new file mode 100644 index 0000000000..b69b81c605 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/MemoryAttribute.java @@ -0,0 +1,108 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +import java.io.IOException; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * Memory implementation of Attributes + */ +public class MemoryAttribute extends AbstractMemoryHttpData implements Attribute { + + public MemoryAttribute(String name) { + super(name, HttpCodecUtil.DEFAULT_CHARSET, 0); + } + /** + * + * @param name + * @param value + * @throws NullPointerException + * @throws IllegalArgumentException + * @throws IOException + */ + public MemoryAttribute(String name, String value) throws IOException { + super(name, HttpCodecUtil.DEFAULT_CHARSET, 0); // Attribute have no default size + setValue(value); + } + + @Override + public HttpDataType getHttpDataType() { + return HttpDataType.Attribute; + } + + @Override + public String getValue() { + return getChannelBuffer().toString(charset); + } + + @Override + public void setValue(String value) throws IOException { + if (value == null) { + throw new NullPointerException("value"); + } + byte [] bytes = value.getBytes(charset); + ChannelBuffer buffer = ChannelBuffers.wrappedBuffer(bytes); + if (definedSize > 0) { + definedSize = buffer.readableBytes(); + } + setContent(buffer); + } + + @Override + public void addContent(ChannelBuffer 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()); + } + + @Override + 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/src/main/java/org/jboss/netty/handler/codec/http/MemoryFileUpload.java b/src/main/java/org/jboss/netty/handler/codec/http/MemoryFileUpload.java new file mode 100644 index 0000000000..04b3970bd7 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/MemoryFileUpload.java @@ -0,0 +1,126 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +import java.nio.charset.Charset; + +/** + * 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); + } + + @Override + public HttpDataType getHttpDataType() { + return HttpDataType.FileUpload; + } + + @Override + public String getFilename() { + return filename; + } + + @Override + 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()); + } + + @Override + 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; + } + + @Override + public void setContentType(String contentType) { + if (contentType == null) { + throw new NullPointerException("contentType"); + } + this.contentType = contentType; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public String getContentTransferEncoding() { + return contentTransferEncoding; + } + + @Override + 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/src/main/java/org/jboss/netty/handler/codec/http/MixedAttribute.java b/src/main/java/org/jboss/netty/handler/codec/http/MixedAttribute.java new file mode 100644 index 0000000000..44675e53fe --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/MixedAttribute.java @@ -0,0 +1,202 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +import org.jboss.netty.buffer.ChannelBuffer; + +/** + * Mixed implementation using both in Memory and in File with a limit of size + */ +public class MixedAttribute implements Attribute { + private Attribute attribute; + + private 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); + } + } + } + + @Override + public void addContent(ChannelBuffer buffer, boolean last) throws IOException { + if (attribute instanceof MemoryAttribute) { + if (attribute.length() + buffer.readableBytes() > limitSize) { + DiskAttribute diskAttribute = new DiskAttribute(attribute + .getName()); + if (((MemoryAttribute) attribute).getChannelBuffer() != null) { + diskAttribute.addContent(((MemoryAttribute) attribute) + .getChannelBuffer(), last); + } + attribute = diskAttribute; + } + } + attribute.addContent(buffer, last); + } + + @Override + public void delete() { + attribute.delete(); + } + + @Override + public byte[] get() throws IOException { + return attribute.get(); + } + + @Override + public ChannelBuffer getChannelBuffer() throws IOException { + return attribute.getChannelBuffer(); + } + + @Override + public Charset getCharset() { + return attribute.getCharset(); + } + + @Override + public String getString() throws IOException { + return attribute.getString(); + } + + @Override + public String getString(Charset encoding) throws IOException { + return attribute.getString(encoding); + } + + @Override + public boolean isCompleted() { + return attribute.isCompleted(); + } + + @Override + public boolean isInMemory() { + return attribute.isInMemory(); + } + + @Override + public long length() { + return attribute.length(); + } + + @Override + public boolean renameTo(File dest) throws IOException { + return attribute.renameTo(dest); + } + + @Override + public void setCharset(Charset charset) { + attribute.setCharset(charset); + } + + @Override + public void setContent(ChannelBuffer buffer) throws IOException { + if (buffer.readableBytes() > limitSize) { + if (attribute instanceof MemoryAttribute) { + // change to Disk + attribute = new DiskAttribute(attribute.getName()); + } + } + attribute.setContent(buffer); + } + + @Override + 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); + } + + @Override + 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); + } + + @Override + public HttpDataType getHttpDataType() { + return attribute.getHttpDataType(); + } + + @Override + public String getName() { + return attribute.getName(); + } + + @Override + public int compareTo(InterfaceHttpData o) { + return attribute.compareTo(o); + } + + @Override + public String toString() { + return "Mixed: " + attribute.toString(); + } + + @Override + public String getValue() throws IOException { + return attribute.getValue(); + } + + @Override + public void setValue(String value) throws IOException { + attribute.setValue(value); + } + + @Override + public ChannelBuffer getChunk(int length) throws IOException { + return attribute.getChunk(length); + } + + @Override + public File getFile() throws IOException { + return attribute.getFile(); + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/MixedFileUpload.java b/src/main/java/org/jboss/netty/handler/codec/http/MixedFileUpload.java new file mode 100644 index 0000000000..377a451434 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/MixedFileUpload.java @@ -0,0 +1,227 @@ +/* + * Copyright 2011 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 org.jboss.netty.handler.codec.http; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +import org.jboss.netty.buffer.ChannelBuffer; + +/** + * Mixed implementation using both in Memory and in File with a limit of size + */ +public class MixedFileUpload implements FileUpload { + private FileUpload fileUpload; + + private long limitSize; + + private 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; + } + + @Override + public void addContent(ChannelBuffer 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).getChannelBuffer() != null) { + diskFileUpload.addContent(((MemoryFileUpload) fileUpload) + .getChannelBuffer(), last); + } + fileUpload = diskFileUpload; + } + } + fileUpload.addContent(buffer, last); + } + + @Override + public void delete() { + fileUpload.delete(); + } + + @Override + public byte[] get() throws IOException { + return fileUpload.get(); + } + + @Override + public ChannelBuffer getChannelBuffer() throws IOException { + return fileUpload.getChannelBuffer(); + } + + @Override + public Charset getCharset() { + return fileUpload.getCharset(); + } + + @Override + public String getContentType() { + return fileUpload.getContentType(); + } + + @Override + public String getContentTransferEncoding() { + return fileUpload.getContentTransferEncoding(); + } + + @Override + public String getFilename() { + return fileUpload.getFilename(); + } + + @Override + public String getString() throws IOException { + return fileUpload.getString(); + } + + @Override + public String getString(Charset encoding) throws IOException { + return fileUpload.getString(encoding); + } + + @Override + public boolean isCompleted() { + return fileUpload.isCompleted(); + } + + @Override + public boolean isInMemory() { + return fileUpload.isInMemory(); + } + + @Override + public long length() { + return fileUpload.length(); + } + + @Override + public boolean renameTo(File dest) throws IOException { + return fileUpload.renameTo(dest); + } + + @Override + public void setCharset(Charset charset) { + fileUpload.setCharset(charset); + } + + @Override + public void setContent(ChannelBuffer 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); + } + + @Override + 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); + } + + @Override + 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); + } + + @Override + public void setContentType(String contentType) { + fileUpload.setContentType(contentType); + } + + @Override + public void setContentTransferEncoding(String contentTransferEncoding) { + fileUpload.setContentTransferEncoding(contentTransferEncoding); + } + + @Override + public void setFilename(String filename) { + fileUpload.setFilename(filename); + } + + @Override + public HttpDataType getHttpDataType() { + return fileUpload.getHttpDataType(); + } + + @Override + public String getName() { + return fileUpload.getName(); + } + + @Override + public int compareTo(InterfaceHttpData o) { + return fileUpload.compareTo(o); + } + + @Override + public String toString() { + return "Mixed: " + fileUpload.toString(); + } + + @Override + public ChannelBuffer getChunk(int length) throws IOException { + return fileUpload.getChunk(length); + } + + @Override + public File getFile() throws IOException { + return fileUpload.getFile(); + } + +}