You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@olingo.apache.org by ch...@apache.org on 2014/11/18 15:46:47 UTC

[2/7] olingo-odata4 git commit: [OLINGO-472] Batch Refactoring

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/51acf8ae/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/BatchRequestParserTest.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/BatchRequestParserTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/BatchRequestParserTest.java
new file mode 100644
index 0000000..68a219a
--- /dev/null
+++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/BatchRequestParserTest.java
@@ -0,0 +1,1326 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.olingo.server.core.deserializer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.util.List;
+
+import org.apache.olingo.commons.api.http.HttpHeader;
+import org.apache.olingo.commons.api.http.HttpMethod;
+import org.apache.olingo.server.api.ODataRequest;
+import org.apache.olingo.server.api.batch.BatchException;
+import org.apache.olingo.server.api.batch.BatchException.MessageKeys;
+import org.apache.olingo.server.api.deserializer.batch.BatchRequestPart;
+import org.apache.olingo.server.core.deserializer.batch.BatchParser;
+import org.apache.olingo.server.core.deserializer.batch.BatchParserCommon;
+import org.junit.Test;
+
+public class BatchRequestParserTest {
+
+  private static final String SERVICE_ROOT = "http://localhost/odata";
+  private static final String CONTENT_TYPE = "multipart/mixed;boundary=batch_8194-cf13-1f56";
+  private static final String CRLF = "\r\n";
+  private static final String MIME_HEADERS = "Content-Type: application/http" + CRLF
+      + "Content-Transfer-Encoding: binary" + CRLF;
+  private static final String GET_REQUEST = ""
+      + MIME_HEADERS
+      + CRLF
+      + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
+      + CRLF
+      + CRLF;
+
+  @Test
+  public void test() throws IOException, BatchException, URISyntaxException {
+    final InputStream in = readFile("/batchWithPost.batch");
+    final List<BatchRequestPart> batchRequestParts = parse(in);
+
+    assertNotNull(batchRequestParts);
+    assertFalse(batchRequestParts.isEmpty());
+
+    for (BatchRequestPart object : batchRequestParts) {
+      if (!object.isChangeSet()) {
+        assertEquals(1, object.getRequests().size());
+        ODataRequest retrieveRequest = object.getRequests().get(0);
+        assertEquals(HttpMethod.GET, retrieveRequest.getMethod());
+
+        if (retrieveRequest.getHeaders(HttpHeader.ACCEPT_LANGUAGE) != null) {
+          assertEquals(3, retrieveRequest.getHeaders(HttpHeader.ACCEPT_LANGUAGE).size());
+        }
+
+        assertEquals(SERVICE_ROOT, retrieveRequest.getRawBaseUri());
+        assertEquals("/Employees('2')/EmployeeName", retrieveRequest.getRawODataPath());
+        assertEquals("http://localhost/odata/Employees('2')/EmployeeName?$format=json", retrieveRequest
+            .getRawRequestUri());
+        assertEquals("$format=json", retrieveRequest.getRawQueryPath());
+      } else {
+        List<ODataRequest> requests = object.getRequests();
+        for (ODataRequest request : requests) {
+
+          assertEquals(HttpMethod.PUT, request.getMethod());
+          assertEquals("100000", request.getHeader(HttpHeader.CONTENT_LENGTH));
+          assertEquals("application/json;odata=verbose", request.getHeader(HttpHeader.CONTENT_TYPE));
+
+          List<String> acceptHeader = request.getHeaders(HttpHeader.ACCEPT);
+          assertEquals(3, request.getHeaders(HttpHeader.ACCEPT).size());
+          assertEquals("application/atomsvc+xml;q=0.8", acceptHeader.get(0));
+          assertEquals("*/*;q=0.1", acceptHeader.get(2));
+
+          assertEquals("http://localhost/odata/Employees('2')/EmployeeName", request.getRawRequestUri());
+          assertEquals("http://localhost/odata", request.getRawBaseUri());
+          assertEquals("/Employees('2')/EmployeeName", request.getRawODataPath());
+          assertEquals("", request.getRawQueryPath()); // No query parameter
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testImageInContent() throws IOException, BatchException, URISyntaxException {
+    final InputStream contentInputStream = readFile("/batchWithContent.batch");
+    final String content = StringUtil.toString(contentInputStream);
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees?$filter=Age%20gt%2040 HTTP/1.1" + CRLF
+        + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF
+        + "MaxDataServiceVersion: 2.0" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + "content-type:     Application/http" + CRLF
+        + "content-transfer-encoding: Binary" + CRLF
+        + "Content-ID: 1" + CRLF
+        + CRLF
+        + "POST Employees HTTP/1.1" + CRLF
+        + "Content-length: 100000" + CRLF
+        + "Content-type: application/octet-stream" + CRLF
+        + CRLF
+        + content
+        + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + "--batch_8194-cf13-1f56--";
+    final List<BatchRequestPart> BatchRequestParts = parse(batch);
+
+    for (BatchRequestPart part : BatchRequestParts) {
+      if (!part.isChangeSet()) {
+        assertEquals(1, part.getRequests().size());
+        final ODataRequest retrieveRequest = part.getRequests().get(0);
+
+        assertEquals(HttpMethod.GET, retrieveRequest.getMethod());
+        assertEquals("http://localhost/odata/Employees?$filter=Age%20gt%2040", retrieveRequest.getRawRequestUri());
+        assertEquals("http://localhost/odata", retrieveRequest.getRawBaseUri());
+        assertEquals("/Employees", retrieveRequest.getRawODataPath());
+        assertEquals("$filter=Age%20gt%2040", retrieveRequest.getRawQueryPath());
+      } else {
+        final List<ODataRequest> requests = part.getRequests();
+        for (ODataRequest request : requests) {
+          assertEquals(HttpMethod.POST, request.getMethod());
+          assertEquals("100000", request.getHeader(HttpHeader.CONTENT_LENGTH));
+          assertEquals("1", request.getHeader(BatchParserCommon.HTTP_CONTENT_ID));
+          assertEquals("application/octet-stream", request.getHeader(HttpHeader.CONTENT_TYPE));
+
+          final InputStream body = request.getBody();
+          assertEquals(content, StringUtil.toString(body));
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testPostWithoutBody() throws IOException, BatchException, URISyntaxException {
+    final String batch = CRLF
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: changeRequest1" + CRLF
+        + CRLF
+        + "POST Employees('2') HTTP/1.1" + CRLF
+        + "Content-Length: 100" + CRLF
+        + "Content-Type: application/octet-stream" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+    final List<BatchRequestPart> batchRequestParts = parse(batch);
+
+    for (BatchRequestPart object : batchRequestParts) {
+      if (object.isChangeSet()) {
+        final List<ODataRequest> requests = object.getRequests();
+
+        for (ODataRequest request : requests) {
+          assertEquals(HttpMethod.POST, request.getMethod());
+          assertEquals("100", request.getHeader(HttpHeader.CONTENT_LENGTH));
+          assertEquals("application/octet-stream", request.getHeader(HttpHeader.CONTENT_TYPE));
+          assertNotNull(request.getBody());
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testBoundaryParameterWithQuotas() throws BatchException, UnsupportedEncodingException {
+    final String contentType = "multipart/mixed; boundary=\"batch_1.2+34:2j)0?\"";
+    final String batch = ""
+        + "--batch_1.2+34:2j)0?" + CRLF
+        + GET_REQUEST
+        + "--batch_1.2+34:2j)0?--";
+    final BatchParser parser = new BatchParser();
+    final List<BatchRequestPart> batchRequestParts =
+        parser.parseBatchRequest(StringUtil.toInputStream(batch), contentType, SERVICE_ROOT, "", true);
+
+    assertNotNull(batchRequestParts);
+    assertFalse(batchRequestParts.isEmpty());
+  }
+
+  @Test
+  public void testBatchWithInvalidContentType() throws UnsupportedEncodingException {
+    final String invalidContentType = "multipart;boundary=batch_1740-bb84-2f7f";
+    final String batch = ""
+        + "--batch_1740-bb84-2f7f" + CRLF
+        + GET_REQUEST
+        + "--batch_1740-bb84-2f7f--";
+    final BatchParser parser = new BatchParser();
+
+    try {
+      parser.parseBatchRequest(StringUtil.toInputStream(batch), invalidContentType, SERVICE_ROOT, "", true);
+      fail();
+    } catch (BatchException e) {
+      assertMessageKey(e, BatchException.MessageKeys.INVALID_CONTENT_TYPE);
+    }
+  }
+
+  @Test
+  public void testContentTypeCharset() throws BatchException {
+    final String contentType = "multipart/mixed; charset=UTF-8;boundary=batch_14d1-b293-b99a";
+    final String batch = ""
+        + "--batch_14d1-b293-b99a" + CRLF
+        + GET_REQUEST
+        + "--batch_14d1-b293-b99a--";
+    final BatchParser parser = new BatchParser();
+    final List<BatchRequestPart> parts =
+        parser.parseBatchRequest(StringUtil.toInputStream(batch), contentType, SERVICE_ROOT, "", true);
+
+    assertEquals(1, parts.size());
+  }
+
+  @Test
+  public void testBatchWithoutBoundaryParameter() throws UnsupportedEncodingException {
+    final String invalidContentType = "multipart/mixed";
+    final String batch = ""
+        + "--batch_1740-bb84-2f7f" + CRLF
+        + GET_REQUEST
+        + "--batch_1740-bb84-2f7f--";
+    final BatchParser parser = new BatchParser();
+
+    try {
+      parser.parseBatchRequest(StringUtil.toInputStream(batch), invalidContentType, SERVICE_ROOT, "", true);
+      fail();
+    } catch (BatchException e) {
+      assertMessageKey(e, BatchException.MessageKeys.INVALID_CONTENT_TYPE);
+    }
+  }
+
+  @Test
+  public void testBoundaryParameterWithoutQuota() throws UnsupportedEncodingException {
+    final String invalidContentType = "multipart/mixed;boundary=batch_1740-bb:84-2f7f";
+    final String batch = ""
+        + "--batch_1740-bb:84-2f7f" + CRLF
+        + GET_REQUEST
+        + "--batch_1740-bb:84-2f7f--";
+    final BatchParser parser = new BatchParser();
+
+    try {
+      parser.parseBatchRequest(StringUtil.toInputStream(batch), invalidContentType, SERVICE_ROOT, "", true);
+      fail();
+    } catch (BatchException e) {
+      assertMessageKey(e, BatchException.MessageKeys.INVALID_BOUNDARY);
+    }
+  }
+
+  @Test
+  public void testWrongBoundaryString() throws BatchException, UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f5" + CRLF
+        + GET_REQUEST
+        + "--batch_8194-cf13-1f56--";
+
+    final List<BatchRequestPart> parts = parse(batch);
+    assertEquals(0, parts.size());
+  }
+
+  @Test
+  public void testMissingHttpVersion() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding:binary" + CRLF
+        + CRLF
+        + "GET Employees?$format=json" + CRLF
+        + "Host: localhost:8080" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_STATUS_LINE);
+  }
+
+  @Test
+  public void testMissingHttpVersion2() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding:binary" + CRLF
+        + CRLF
+        + "GET Employees?$format=json " + CRLF
+        + "Host: localhost:8080" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_HTTP_VERSION);
+  }
+
+  @Test
+  public void testMissingHttpVersion3() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding:binary" + CRLF
+        + CRLF
+        + "GET Employees?$format=json SMTP:3.1" + CRLF
+        + "Host: localhost:8080" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_HTTP_VERSION);
+  }
+
+  @Test
+  public void testBoundaryWithoutHyphen() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + GET_REQUEST
+        + "batch_8194-cf13-1f56" + CRLF
+        + GET_REQUEST
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CONTENT);
+  }
+
+  @Test
+  public void testNoBoundaryString() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + GET_REQUEST
+        // + no boundary string
+        + GET_REQUEST
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CONTENT);
+  }
+
+  @Test
+  public void testBatchBoundaryEqualsChangeSetBoundary() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed;boundary=batch_8194-cf13-1f56" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "PUT Employees('2')/EmployeeName HTTP/1.1" + CRLF
+        + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "MaxDataServiceVersion: 2.0" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Frederic Fall MODIFIED\"}" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--"
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_BLANK_LINE);
+  }
+
+  @Test
+  public void testNoContentType() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + CRLF
+        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CONTENT_TYPE);
+  }
+
+  @Test
+  public void testMimeHeaderContentType() throws UnsupportedEncodingException {
+    final String batch = "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: text/plain" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + CRLF
+        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CONTENT_TYPE);
+  }
+
+  @Test
+  public void testMimeHeaderEncoding() throws UnsupportedEncodingException {
+    String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: 8bit" + CRLF
+        + CRLF
+        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CONTENT_TRANSFER_ENCODING);
+  }
+
+  @Test
+  public void testGetRequestMissingCRLF() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: 1" + CRLF
+        + CRLF
+        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        // + CRLF // Belongs to the GET request
+        + CRLF // Belongs to the
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_BLANK_LINE);
+  }
+
+  @Test
+  public void testInvalidMethodForBatch() throws UnsupportedEncodingException {
+    final String batch = "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "POST Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_QUERY_OPERATION_METHOD);
+  }
+
+  @Test
+  public void testNoBoundaryFound() throws UnsupportedEncodingException {
+    final String batch = "batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "POST Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + CRLF;
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CLOSE_DELIMITER);
+  }
+
+  @Test
+  public void testEmptyRequest() throws BatchException, UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56--";
+
+    final List<BatchRequestPart> parts = parse(batch);
+    assertEquals(0, parts.size());
+  }
+
+  @Test
+  public void testBadRequest() throws UnsupportedEncodingException {
+    final String batch = "This is a bad request. There is no syntax and also no semantic";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CLOSE_DELIMITER);
+  }
+
+  @Test
+  public void testNoMethod() throws UnsupportedEncodingException {
+    final String batch = "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + /* GET */"Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_STATUS_LINE);
+  }
+
+  @Test
+  public void testInvalidMethodForChangeset() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "GET Employees('2')/EmployeeName HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "MaxDataServiceVersion: 2.0" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd--"
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CHANGESET_METHOD);
+  }
+
+  @Test
+  public void testInvalidChangeSetBoundary() throws UnsupportedEncodingException, BatchException {
+    final String batch = "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94d"/* +"d" */+ CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "POST Employees('2') HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "MaxDataServiceVersion: 2.0" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    final List<BatchRequestPart> parts = parse(batch);
+    assertEquals(1, parts.size());
+
+    final BatchRequestPart part = parts.get(0);
+    assertTrue(part.isChangeSet());
+    assertEquals(0, part.getRequests().size());
+  }
+
+  @Test
+  public void testNestedChangeset() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd2" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd2" + CRLF
+        + MIME_HEADERS
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "POST Employees('2') HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "MaxDataServiceVersion: 2.0" + CRLF
+        + "Content-Id: 2"
+        + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CONTENT_TYPE);
+  }
+
+  @Test
+  public void testMissingContentTransferEncoding() throws UnsupportedEncodingException {
+    final String batch = "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + "Content-Id: 1" + CRLF
+        + "Content-Type: application/http" + CRLF
+        // + "Content-Transfer-Encoding: binary" + CRLF
+        + CRLF
+        + "POST Employees('2') HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "MaxDataServiceVersion: 2.0" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CONTENT_TRANSFER_ENCODING);
+  }
+
+  @Test
+  public void testMissingContentType() throws UnsupportedEncodingException {
+    final String batch = "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + "Content-Id: 1"
+        // + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + CRLF
+        + "POST Employees('2') HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "MaxDataServiceVersion: 2.0" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CONTENT_TYPE);
+  }
+
+  @Test
+  public void testNoCloseDelimiter() throws BatchException, UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + GET_REQUEST;
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CLOSE_DELIMITER);
+  }
+
+  @Test
+  public void testNoCloseDelimiter2() throws BatchException, UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF;
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CLOSE_DELIMITER);
+  }
+
+  @Test
+  public void testInvalidUri() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET http://localhost/aa/odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_URI);
+  }
+
+  @Test
+  public void testUriWithAbsolutePath() throws BatchException, UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET /odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + "Host: http://localhost" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    final List<BatchRequestPart> parts = parse(batch);
+    assertEquals(1, parts.size());
+
+    final BatchRequestPart part = parts.get(0);
+    assertEquals(1, part.getRequests().size());
+    final ODataRequest request = part.getRequests().get(0);
+
+    assertEquals("http://localhost/odata/Employees('1')/EmployeeName", request.getRawRequestUri());
+    assertEquals("http://localhost/odata", request.getRawBaseUri());
+    assertEquals("/Employees('1')/EmployeeName", request.getRawODataPath());
+    assertEquals("", request.getRawQueryPath());
+  }
+
+  @Test
+  public void testUriWithAbsolutePathMissingHostHeader() throws BatchException, UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET /odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, MessageKeys.MISSING_MANDATORY_HEADER);
+  }
+
+  @Test
+  public void testUriWithAbsolutePathMissingHostDulpicatedHeader() throws BatchException, UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET /odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + "Host: http://localhost" + CRLF
+        + "Host: http://localhost/odata" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, MessageKeys.MISSING_MANDATORY_HEADER);
+  }
+
+  @Test
+  public void testUriWithAbsolutePathOtherHost() throws BatchException, UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET /odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + "Host: http://localhost2" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, MessageKeys.INVALID_URI);
+  }
+
+  @Test
+  public void testUriWithAbsolutePathWrongPath() throws BatchException, UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET /myservice/Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + "Host: http://localhost" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, MessageKeys.INVALID_URI);
+  }
+
+  @Test
+  public void testNoCloseDelimiter3() throws UnsupportedEncodingException {
+    final String batch = "--batch_8194-cf13-1f56" + CRLF + GET_REQUEST + "--batch_8194-cf13-1f56-"/* no hyphen */;
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CLOSE_DELIMITER);
+  }
+
+  @Test
+  public void testNegativeContentLengthChangeSet() throws BatchException, IOException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: 1" + CRLF
+        + "Content-Length: -2" + CRLF
+        + CRLF
+        + "PUT EmployeeName HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parse(batch);
+  }
+
+  @Test
+  public void testNegativeContentLengthRequest() throws BatchException, IOException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: 1" + CRLF
+        + CRLF
+        + "PUT EmployeeName HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "Content-Id: 1" + CRLF
+        + "Content-Length: 2" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parse(batch);
+  }
+
+  @Test
+  public void testContentLengthGreatherThanBodyLength() throws BatchException, IOException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: 1" + CRLF
+        + CRLF
+        + "PUT Employee/Name HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "Content-Length: 100000" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+    final List<BatchRequestPart> batchRequestParts = parse(batch);
+
+    assertNotNull(batchRequestParts);
+
+    for (BatchRequestPart multipart : batchRequestParts) {
+      if (multipart.isChangeSet()) {
+        assertEquals(1, multipart.getRequests().size());
+
+        final ODataRequest request = multipart.getRequests().get(0);
+        assertEquals("{\"EmployeeName\":\"Peter Fall\"}", StringUtil.toString(request.getBody()));
+      }
+    }
+  }
+
+  @Test
+  public void testContentLengthSmallerThanBodyLength() throws BatchException, IOException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: 1" + CRLF
+        + CRLF
+        + "PUT EmployeeName HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "Content-Length: 10" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+    final List<BatchRequestPart> batchRequestParts = parse(batch);
+
+    assertNotNull(batchRequestParts);
+
+    for (BatchRequestPart multipart : batchRequestParts) {
+      if (multipart.isChangeSet()) {
+        assertEquals(1, multipart.getRequests().size());
+
+        final ODataRequest request = multipart.getRequests().get(0);
+        assertEquals("{\"Employee", StringUtil.toString(request.getBody()));
+      }
+    }
+  }
+
+  @Test
+  public void testNonNumericContentLength() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: 1" + CRLF
+        + CRLF
+        + "PUT EmployeeName HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "Content-Length: 10abc" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_HEADER);
+  }
+
+  @Test
+  public void testNonStrictParser() throws BatchException, IOException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed;boundary=changeset_8194-cf13-1f56" + CRLF
+        + "--changeset_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: myRequest" + CRLF
+        + "PUT Employees('2')/EmployeeName HTTP/1.1" + CRLF
+        + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "MaxDataServiceVersion: 2.0" + CRLF
+        + "{\"EmployeeName\":\"Frederic Fall MODIFIED\"}" + CRLF
+        + "--changeset_8194-cf13-1f56--" + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    final List<BatchRequestPart> requests = parse(batch, false);
+
+    assertNotNull(requests);
+    assertEquals(1, requests.size());
+
+    final BatchRequestPart part = requests.get(0);
+    assertTrue(part.isChangeSet());
+    assertNotNull(part.getRequests());
+    assertEquals(1, part.getRequests().size());
+
+    final ODataRequest changeRequest = part.getRequests().get(0);
+    assertEquals("{\"EmployeeName\":\"Frederic Fall MODIFIED\"}",
+        StringUtil.toString(changeRequest.getBody()));
+    assertEquals("application/json;odata=verbose", changeRequest.getHeader(HttpHeader.CONTENT_TYPE));
+    assertEquals(HttpMethod.PUT, changeRequest.getMethod());
+  }
+
+  @Test
+  public void testNonStrictParserMoreCRLF() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed;boundary=changeset_8194-cf13-1f56" + CRLF
+        + "--changeset_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + CRLF // Only one CRLF allowed
+        + "PUT Employees('2')/EmployeeName HTTP/1.1" + CRLF
+        + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "MaxDataServiceVersion: 2.0" + CRLF
+        + "{\"EmployeeName\":\"Frederic Fall MODIFIED\"}" + CRLF
+        + "--changeset_8194-cf13-1f56--" + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_STATUS_LINE, false);
+  }
+
+  @Test
+  public void testContentId() throws BatchException, UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees HTTP/1.1" + CRLF
+        + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF
+        + "Content-Id: BBB" + CRLF
+        + CRLF + CRLF
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "POST Employees HTTP/1.1" + CRLF
+        + "Content-type: application/octet-stream" + CRLF
+        + CRLF
+        + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "PUT $1/EmployeeName HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + "Content-Id: 2" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    final List<BatchRequestPart> batchRequestParts = parse(batch);
+    assertNotNull(batchRequestParts);
+
+    for (BatchRequestPart multipart : batchRequestParts) {
+      if (!multipart.isChangeSet()) {
+        assertEquals(1, multipart.getRequests().size());
+        final ODataRequest retrieveRequest = multipart.getRequests().get(0);
+
+        assertEquals("BBB", retrieveRequest.getHeader(BatchParserCommon.HTTP_CONTENT_ID));
+      } else {
+        for (ODataRequest request : multipart.getRequests()) {
+          if (HttpMethod.POST.equals(request.getMethod())) {
+            assertEquals("1", request.getHeader(BatchParserCommon.HTTP_CONTENT_ID));
+          } else if (HttpMethod.PUT.equals(request.getMethod())) {
+            assertEquals("2", request.getHeader(BatchParserCommon.HTTP_CONTENT_ID));
+            assertEquals("/$1/EmployeeName", request.getRawODataPath());
+            assertEquals("http://localhost/odata/$1/EmployeeName", request.getRawRequestUri());
+          }
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testNoContentId() throws BatchException, UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees HTTP/1.1" + CRLF
+        + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF
+        + CRLF + CRLF
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "POST Employees HTTP/1.1" + CRLF
+        + "Content-type: application/octet-stream" + CRLF
+        + CRLF
+        + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "PUT $1/EmployeeName HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parse(batch);
+  }
+
+  @Test
+  public void testPreamble() throws BatchException, IOException {
+    final String batch = ""
+        + "This is a preamble and must be ignored" + CRLF
+        + CRLF
+        + CRLF
+        + "----1242" + CRLF
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees HTTP/1.1" + CRLF
+        + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF
+        + "Content-Id: BBB" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "This is a preamble and must be ignored" + CRLF
+        + CRLF
+        + CRLF
+        + "----1242" + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "POST Employees HTTP/1.1" + CRLF
+        + "Content-type: application/octet-stream" + CRLF
+        + CRLF
+        + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: 2" + CRLF
+        + CRLF
+        + "PUT $1/EmployeeName HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+    final List<BatchRequestPart> batchRequestParts = parse(batch);
+
+    assertNotNull(batchRequestParts);
+    assertEquals(2, batchRequestParts.size());
+
+    final BatchRequestPart getRequestPart = batchRequestParts.get(0);
+    assertEquals(1, getRequestPart.getRequests().size());
+
+    final ODataRequest getRequest = getRequestPart.getRequests().get(0);
+    assertEquals(HttpMethod.GET, getRequest.getMethod());
+
+    final BatchRequestPart changeSetPart = batchRequestParts.get(1);
+    assertEquals(2, changeSetPart.getRequests().size());
+    assertEquals("/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA"
+        + CRLF,
+        StringUtil.toString(changeSetPart.getRequests().get(0).getBody()));
+    assertEquals("{\"EmployeeName\":\"Peter Fall\"}",
+        StringUtil.toString(changeSetPart.getRequests().get(1).getBody()));
+  }
+
+  @Test
+  public void testContentTypeCaseInsensitive() throws BatchException, IOException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: muLTiParT/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: 1" + CRLF
+        + "Content-Length: 200" + CRLF
+        + CRLF
+        + "PUT EmployeeName HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parse(batch);
+  }
+
+  @Test
+  public void testContentTypeBoundaryCaseInsensitive() throws BatchException, IOException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; bOunDaRy=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: 1" + CRLF
+        + CRLF
+        + "PUT EmployeeName HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+    final List<BatchRequestPart> batchRequestParts = parse(batch);
+
+    assertNotNull(batchRequestParts);
+    assertEquals(1, batchRequestParts.size());
+    assertTrue(batchRequestParts.get(0).isChangeSet());
+    assertEquals(1, batchRequestParts.get(0).getRequests().size());
+  }
+
+  @Test
+  public void testEpilog() throws BatchException, IOException {
+    String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees HTTP/1.1" + CRLF
+        + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF
+        + "Content-Id: BBB" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "POST Employees HTTP/1.1" + CRLF
+        + "Content-type: application/octet-stream" + CRLF
+        + CRLF
+        + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF
+        + CRLF
+        + "--changeset_f980-1cb6-94dd" + CRLF
+        + MIME_HEADERS
+        + "Content-ID: 2" + CRLF
+        + CRLF
+        + "PUT $1/EmployeeName HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
+        + "--changeset_f980-1cb6-94dd--" + CRLF
+        + CRLF
+        + "This is an epilog and must be ignored" + CRLF
+        + CRLF
+        + CRLF
+        + "----1242"
+        + CRLF
+        + "--batch_8194-cf13-1f56--"
+        + CRLF
+        + "This is an epilog and must be ignored" + CRLF
+        + CRLF
+        + CRLF
+        + "----1242";
+    final List<BatchRequestPart> batchRequestParts = parse(batch);
+
+    assertNotNull(batchRequestParts);
+    assertEquals(2, batchRequestParts.size());
+
+    BatchRequestPart getRequestPart = batchRequestParts.get(0);
+    assertEquals(1, getRequestPart.getRequests().size());
+    ODataRequest getRequest = getRequestPart.getRequests().get(0);
+    assertEquals(HttpMethod.GET, getRequest.getMethod());
+
+    BatchRequestPart changeSetPart = batchRequestParts.get(1);
+    assertEquals(2, changeSetPart.getRequests().size());
+    assertEquals("/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA"
+        + CRLF,
+        StringUtil.toString(changeSetPart.getRequests().get(0).getBody()));
+    assertEquals("{\"EmployeeName\":\"Peter Fall\"}",
+        StringUtil.toString(changeSetPart.getRequests().get(1).getBody()));
+  }
+
+  @Test
+  public void testLargeBatch() throws BatchException, IOException {
+    final InputStream in = readFile("/batchLarge.batch");
+    parse(in);
+  }
+
+  @Test
+  public void testForddenHeaderAuthorisation() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + "Authorization: Basic QWxhZdsdsddsduIHNlc2FtZQ==" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER);
+  }
+
+  @Test
+  public void testForddenHeaderExpect() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + "Expect: 100-continue" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER);
+  }
+
+  @Test
+  public void testForddenHeaderFrom() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + "From: test@test.com" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER);
+  }
+
+  @Test
+  public void testForddenHeaderRange() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + "Range: 200-256" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER);
+  }
+
+  @Test
+  public void testForddenHeaderMaxForwards() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + "Max-Forwards: 3" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER);
+  }
+
+  @Test
+  public void testForddenHeaderTE() throws UnsupportedEncodingException {
+    final String batch = ""
+        + "--batch_8194-cf13-1f56" + CRLF
+        + MIME_HEADERS
+        + CRLF
+        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
+        + "TE: deflate" + CRLF
+        + CRLF
+        + CRLF
+        + "--batch_8194-cf13-1f56--";
+
+    parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER);
+  }
+
+  private List<BatchRequestPart> parse(final InputStream in, final boolean isStrict) throws BatchException {
+    final BatchParser parser = new BatchParser();
+    final List<BatchRequestPart> batchRequestParts =
+        parser.parseBatchRequest(in, CONTENT_TYPE, SERVICE_ROOT, "", isStrict);
+
+    assertNotNull(batchRequestParts);
+
+    return batchRequestParts;
+  }
+
+  private List<BatchRequestPart> parse(final InputStream in) throws BatchException {
+    return parse(in, true);
+  }
+
+  private List<BatchRequestPart> parse(final String batch) throws BatchException, UnsupportedEncodingException {
+    return parse(batch, true);
+  }
+
+  private List<BatchRequestPart> parse(final String batch, final boolean isStrict) throws BatchException,
+      UnsupportedEncodingException {
+    return parse(StringUtil.toInputStream(batch), isStrict);
+  }
+
+  private void parseInvalidBatchBody(final String batch, final MessageKeys key, final boolean isStrict)
+      throws UnsupportedEncodingException {
+    final BatchParser parser = new BatchParser();
+
+    try {
+      parser.parseBatchRequest(StringUtil.toInputStream(batch), CONTENT_TYPE, SERVICE_ROOT, "", isStrict);
+      fail("No exception thrown. Expect: " + key.toString());
+    } catch (BatchException e) {
+      assertMessageKey(e, key);
+    }
+  }
+
+  private void parseInvalidBatchBody(final String batch, final MessageKeys key) throws UnsupportedEncodingException {
+    parseInvalidBatchBody(batch, key, true);
+  }
+
+  private void assertMessageKey(final BatchException e, final MessageKeys key) {
+    assertEquals(key, e.getMessageKey());
+  }
+
+  private InputStream readFile(final String fileName) throws IOException {
+    final InputStream in = ClassLoader.class.getResourceAsStream(fileName);
+    if (in == null) {
+      throw new IOException("Requested file '" + fileName + "' was not found.");
+    }
+    return in;
+  }
+}

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/51acf8ae/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/BufferedReaderIncludingLineEndingsTest.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/BufferedReaderIncludingLineEndingsTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/BufferedReaderIncludingLineEndingsTest.java
new file mode 100644
index 0000000..d622600
--- /dev/null
+++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/BufferedReaderIncludingLineEndingsTest.java
@@ -0,0 +1,484 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.olingo.server.core.deserializer;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+
+import org.apache.olingo.server.core.deserializer.batch.BufferedReaderIncludingLineEndings;
+import org.apache.olingo.server.core.deserializer.batch.BufferedReaderIncludingLineEndings.Line;
+import org.junit.Test;
+
+public class BufferedReaderIncludingLineEndingsTest {
+
+
+  private static final String TEXT_COMBINED = "Test\r" +
+      "Test2\r\n" +
+      "Test3\n" +
+      "Test4\r" +
+      "\r" +
+      "\r\n" +
+      "\r\n" +
+      "Test5\n" +
+      "Test6\r\n" +
+      "Test7\n" +
+      "\n";
+
+  private static final String TEXT_SMALL = "Test\r" +
+      "123";
+  private static final String TEXT_EMPTY = "";
+
+  @Test
+  public void testSimpleText() throws IOException {
+    final String TEXT = "Test";
+    BufferedReaderIncludingLineEndings reader = create(TEXT);
+
+    assertEquals(TEXT, reader.readLine());
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testNoText() throws IOException {
+    final String TEXT = "";
+    BufferedReaderIncludingLineEndings reader = create(TEXT);
+
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testNoBytes() throws IOException {
+    BufferedReaderIncludingLineEndings reader =
+        new BufferedReaderIncludingLineEndings(new InputStreamReader(new ByteArrayInputStream(new byte[0])));
+
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testCRLF() throws IOException {
+    final String TEXT = "Test\r\n" +
+        "Test2";
+
+    BufferedReaderIncludingLineEndings reader = create(TEXT);
+
+    assertEquals("Test\r\n", reader.readLine());
+    assertEquals("Test2", reader.readLine());
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testLF() throws IOException {
+    final String TEXT = "Test\n" +
+        "Test2";
+
+    BufferedReaderIncludingLineEndings reader = create(TEXT);
+
+    assertEquals("Test\n", reader.readLine());
+    assertEquals("Test2", reader.readLine());
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testCR() throws IOException {
+    final String TEXT = "Test\r" +
+        "Test2";
+
+    BufferedReaderIncludingLineEndings reader = create(TEXT);
+
+    assertEquals("Test\r", reader.readLine());
+    assertEquals("Test2", reader.readLine());
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testCombined() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_COMBINED);
+
+    assertEquals("Test\r", reader.readLine());
+    assertEquals("Test2\r\n", reader.readLine());
+    assertEquals("Test3\n", reader.readLine());
+    assertEquals("Test4\r", reader.readLine());
+    assertEquals("\r", reader.readLine());
+    assertEquals("\r\n", reader.readLine());
+    assertEquals("\r\n", reader.readLine());
+    assertEquals("Test5\n", reader.readLine());
+    assertEquals("Test6\r\n", reader.readLine());
+    assertEquals("Test7\n", reader.readLine());
+    assertEquals("\n", reader.readLine());
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testCombinedBufferSizeTwo() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_COMBINED, 2);
+
+    assertEquals("Test\r", reader.readLine());
+    assertEquals("Test2\r\n", reader.readLine());
+    assertEquals("Test3\n", reader.readLine());
+    assertEquals("Test4\r", reader.readLine());
+    assertEquals("\r", reader.readLine());
+    assertEquals("\r\n", reader.readLine());
+    assertEquals("\r\n", reader.readLine());
+    assertEquals("Test5\n", reader.readLine());
+    assertEquals("Test6\r\n", reader.readLine());
+    assertEquals("Test7\n", reader.readLine());
+    assertEquals("\n", reader.readLine());
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testCombinedBufferSizeOne() throws IOException {
+    final String TEXT = "Test\r" +
+        "Test2\r\n" +
+        "Test3\n" +
+        "Test4\r" +
+        "\r" +
+        "\r\n" +
+        "\r\n" +
+        "Test5\n" +
+        "Test6\r\n" +
+        "Test7\n" +
+        "\r\n";
+
+    BufferedReaderIncludingLineEndings reader = create(TEXT, 1);
+
+    assertEquals("Test\r", reader.readLine());
+    assertEquals("Test2\r\n", reader.readLine());
+    assertEquals("Test3\n", reader.readLine());
+    assertEquals("Test4\r", reader.readLine());
+    assertEquals("\r", reader.readLine());
+    assertEquals("\r\n", reader.readLine());
+    assertEquals("\r\n", reader.readLine());
+    assertEquals("Test5\n", reader.readLine());
+    assertEquals("Test6\r\n", reader.readLine());
+    assertEquals("Test7\n", reader.readLine());
+    assertEquals("\r\n", reader.readLine());
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+
+    reader.close();
+  }
+
+  @Test
+  public void testDoubleLF() throws IOException {
+    final String TEXT = "Test\r" +
+        "\r";
+
+    BufferedReaderIncludingLineEndings reader = create(TEXT, 1);
+
+    assertEquals("Test\r", reader.readLine());
+    assertEquals("\r", reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testSkipSimple() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL);
+
+    assertEquals(5, reader.skip(5)); // Test\r
+    assertEquals("123", reader.readLine());
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testSkipBufferOne() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL, 1);
+
+    assertEquals(5, reader.skip(5)); // Test\r
+    assertEquals("123", reader.readLine());
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testReadThanSkip() throws IOException {
+    final String TEXT = "Test\r" +
+        "\r" +
+        "123";
+
+    BufferedReaderIncludingLineEndings reader = create(TEXT);
+
+    assertEquals("Test\r", reader.readLine());
+    assertEquals(1, reader.skip(1)); // Test\r
+    assertEquals("123", reader.readLine());
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testReadMoreBufferCapacityThanCharacterAvailable() throws IOException {
+    final String TEXT = "Foo";
+    char[] buffer = new char[20];
+
+    BufferedReaderIncludingLineEndings reader = create(TEXT);
+    assertEquals(3, reader.read(buffer, 0, 20));
+    assertEquals(-1, reader.read(buffer, 0, 20));
+    reader.close();
+
+    BufferedReaderIncludingLineEndings readerBufferOne = create(TEXT, 1);
+    assertEquals(3, readerBufferOne.read(buffer, 0, 20));
+    assertEquals(-1, readerBufferOne.read(buffer, 0, 20));
+    readerBufferOne.close();
+  }
+
+  @Test
+  public void testSkipZero() throws IOException {
+    final String TEXT = "Test\r" +
+        "123\r\n";
+
+    BufferedReaderIncludingLineEndings reader = create(TEXT);
+
+    assertEquals(0, reader.skip(0)); // Test\r
+    assertEquals("Test\r", reader.readLine());
+    assertEquals("123\r\n", reader.readLine());
+    assertNull(reader.readLine());
+    assertNull(reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testSkipToMuch() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL);
+
+    assertEquals(8, reader.skip(10)); // Test\r
+    assertEquals(null, reader.readLine());
+    reader.close();
+  }
+
+  @Test
+  public void testReadBufferOne() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL, 1);
+
+    assertEquals('T', reader.read());
+    assertEquals('e', reader.read());
+    assertEquals('s', reader.read());
+    assertEquals('t', reader.read());
+    assertEquals('\r', reader.read());
+    assertEquals('1', reader.read());
+    assertEquals('2', reader.read());
+    assertEquals('3', reader.read());
+    assertEquals(-1, reader.read());
+    assertEquals(-1, reader.read());
+  }
+
+  @Test
+  public void testReadZeroBytes() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL, 1);
+
+    char[] buffer = new char[3];
+    assertEquals(0, reader.read(buffer, 0, 0));
+    assertEquals('T', reader.read());
+    assertEquals(0, reader.read(buffer, 0, 0));
+    assertEquals("est\r", reader.readLine());
+    assertEquals("123", reader.readLine());
+
+    reader.close();
+  }
+
+  @Test
+  public void testRead() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL);
+
+    assertEquals('T', reader.read());
+    assertEquals('e', reader.read());
+    assertEquals('s', reader.read());
+    assertEquals('t', reader.read());
+    assertEquals('\r', reader.read());
+    assertEquals('1', reader.read());
+    assertEquals('2', reader.read());
+    assertEquals('3', reader.read());
+    assertEquals(-1, reader.read());
+    assertEquals(-1, reader.read());
+  }
+
+  @Test(expected = IndexOutOfBoundsException.class)
+  public void testFailReadBufferAndOffsetBiggerThanBuffer() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create("");
+
+    final char[] buffer = new char[3];
+    reader.read(buffer, 1, 3);
+  }
+
+  @Test(expected = IndexOutOfBoundsException.class)
+  public void testFailLengthNegative() throws IOException {
+    final char[] buffer = new char[3];
+    BufferedReaderIncludingLineEndings reader = create("123");
+
+    reader.read(buffer, 1, -2);
+    reader.close();
+  }
+
+  @Test(expected = IndexOutOfBoundsException.class)
+  public void testFailOffsetNegative() throws IOException {
+    final char[] buffer = new char[3];
+    BufferedReaderIncludingLineEndings reader = create("123");
+
+    reader.read(buffer, -1, 2);
+    reader.close();
+  }
+
+  @Test
+  public void testReadAndReadLine() throws IOException {
+    final String TEXT = "Test\r" +
+        "bar\n" +
+        "123\r\n" +
+        "foo";
+
+    BufferedReaderIncludingLineEndings reader = create(TEXT);
+
+    assertEquals('T', reader.read());
+    assertEquals('e', reader.read());
+    assertEquals('s', reader.read());
+    assertEquals('t', reader.read());
+    assertEquals("\r", reader.readLine());
+    assertEquals("bar\n", reader.readLine());
+    assertEquals('1', reader.read());
+    assertEquals('2', reader.read());
+    assertEquals("3\r\n", reader.readLine());
+    assertEquals("foo", reader.readLine());
+    assertEquals(null, reader.readLine());
+    assertEquals(-1, reader.read());
+  }
+  
+  @Test
+  public void testLineEqualsAndHashCode() {
+    Line l1 = new Line("The first line", 1);
+    Line l2 = new Line("The first line", 1);
+    Line l3 = new Line("The second line", 2);
+    
+    assertEquals(l1, l2);
+    assertFalse(l1.equals(l3));
+    assertTrue(l1.hashCode() != l3.hashCode());
+  }
+  
+  @Test(expected = IllegalArgumentException.class)
+  public void testSkipNegative() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create("123");
+    reader.skip(-1);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFailBufferSizeZero() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_EMPTY, 0);
+    reader.close();
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testInputStreamIsNull() throws IOException {
+    // Same behaviour like BufferedReader
+    BufferedReaderIncludingLineEndings reader = new BufferedReaderIncludingLineEndings(null);
+    reader.close();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFailBufferSizeNegative() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_EMPTY, -1);
+    reader.close();
+  }
+
+  @Test
+  public void testMarkSupoorted() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_EMPTY);
+
+    assertEquals(false, reader.markSupported());
+    reader.close();
+  }
+
+  @Test(expected = IOException.class)
+  public void testFailMark() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create("123");
+
+    reader.mark(1);
+  }
+
+  @Test(expected = IOException.class)
+  public void testFailReset() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create("123");
+
+    reader.reset();
+  }
+
+  @Test
+  public void testReady() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create("123\r123");
+    assertEquals(false, reader.ready());
+    assertEquals("123\r", reader.readLine());
+    assertEquals(true, reader.ready());
+    assertEquals("123", reader.readLine());
+    assertEquals(false, reader.ready());
+
+    reader.close();
+  }
+
+  @Test
+  public void testToList() throws IOException {
+    BufferedReaderIncludingLineEndings reader = create(TEXT_COMBINED);
+    List<Line> stringList = reader.toLineList();
+
+    assertEquals(11, stringList.size());
+    assertEquals("Test\r", stringList.get(0).toString());
+    assertEquals("Test2\r\n", stringList.get(1).toString());
+    assertEquals("Test3\n", stringList.get(2).toString());
+    assertEquals("Test4\r", stringList.get(3).toString());
+    assertEquals("\r", stringList.get(4).toString());
+    assertEquals("\r\n", stringList.get(5).toString());
+    assertEquals("\r\n", stringList.get(6).toString());
+    assertEquals("Test5\n", stringList.get(7).toString());
+    assertEquals("Test6\r\n", stringList.get(8).toString());
+    assertEquals("Test7\n", stringList.get(9).toString());
+    assertEquals("\n", stringList.get(10).toString());
+    reader.close();
+  }
+
+  private BufferedReaderIncludingLineEndings create(final String inputString) throws UnsupportedEncodingException {
+    return new BufferedReaderIncludingLineEndings(new InputStreamReader(new ByteArrayInputStream(inputString
+        .getBytes("UTF-8"))));
+  }
+
+  private BufferedReaderIncludingLineEndings create(final String inputString, int bufferSize)
+      throws UnsupportedEncodingException {
+    return new BufferedReaderIncludingLineEndings(new InputStreamReader(new ByteArrayInputStream(inputString
+        .getBytes("UTF-8"))), bufferSize);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/51acf8ae/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/HeaderTest.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/HeaderTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/HeaderTest.java
new file mode 100644
index 0000000..369e21e
--- /dev/null
+++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/HeaderTest.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.olingo.server.core.deserializer;
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.olingo.commons.api.http.HttpContentType;
+import org.apache.olingo.commons.api.http.HttpHeader;
+import org.apache.olingo.server.core.deserializer.batch.BatchParserCommon;
+import org.apache.olingo.server.core.deserializer.batch.Header;
+import org.junit.Test;
+
+public class HeaderTest {
+
+  @Test
+  public void test() {
+    Header header = new Header(1);
+    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1);
+
+    assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeader(HttpHeader.CONTENT_TYPE));
+    assertEquals(1, header.getHeaders(HttpHeader.CONTENT_TYPE).size());
+    assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeaders(HttpHeader.CONTENT_TYPE).get(0));
+  }
+
+  @Test
+  public void testNotAvailable() {
+    Header header = new Header(1);
+
+    assertNull(header.getHeader(HttpHeader.CONTENT_TYPE));
+    assertEquals(0, header.getHeaders(HttpHeader.CONTENT_TYPE).size());
+    assertEquals("", header.getHeaderNotNull(HttpHeader.CONTENT_TYPE));
+  }
+
+  @Test
+  public void testCaseInsensitive() {
+    Header header = new Header(1);
+    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1);
+
+    assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeader("cOnTenT-TyPE"));
+    assertEquals(1, header.getHeaders("cOnTenT-TyPE").size());
+    assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeaders("cOnTenT-TyPE").get(0));
+  }
+
+  @Test
+  public void testDuplicatedAdd() {
+    Header header = new Header(1);
+    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1);
+    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 2);
+
+    assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeader(HttpHeader.CONTENT_TYPE));
+    assertEquals(1, header.getHeaders(HttpHeader.CONTENT_TYPE).size());
+    assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeaders(HttpHeader.CONTENT_TYPE).get(0));
+  }
+
+  @Test
+  public void testMatcher() {
+    Header header = new Header(1);
+    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED + ";boundary=123", 1);
+
+    assertTrue(header.isHeaderMatching(HttpHeader.CONTENT_TYPE, BatchParserCommon.PATTERN_MULTIPART_BOUNDARY));
+  }
+
+  @Test
+  public void testFieldName() {
+    Header header = new Header(0);
+    header.addHeader("MyFieldNamE", "myValue", 1);
+
+    assertEquals("MyFieldNamE", header.getHeaderField("myfieldname").getFieldName());
+    assertEquals("MyFieldNamE", header.toSingleMap().keySet().toArray(new String[0])[0]);
+    assertEquals("MyFieldNamE", header.toMultiMap().keySet().toArray(new String[0])[0]);
+
+    assertEquals("myValue", header.toMultiMap().get("MyFieldNamE").get(0));
+    assertEquals("myValue", header.toSingleMap().get("MyFieldNamE"));
+  }
+
+  @Test
+  public void testDeepCopy() {
+    Header header = new Header(1);
+    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED + ";boundary=123", 1);
+
+    Header copy = header.clone();
+    assertEquals(header.getHeaders(HttpHeader.CONTENT_TYPE), copy.getHeaders(HttpHeader.CONTENT_TYPE));
+    assertEquals(header.getHeader(HttpHeader.CONTENT_TYPE), copy.getHeader(HttpHeader.CONTENT_TYPE));
+    assertEquals(header.getHeaderField(HttpHeader.CONTENT_TYPE), copy.getHeaderField(HttpHeader.CONTENT_TYPE));
+
+    assertTrue(header.getHeaders(HttpHeader.CONTENT_TYPE) != copy.getHeaders(HttpHeader.CONTENT_TYPE));
+    assertTrue(header.getHeaderField(HttpHeader.CONTENT_TYPE) != copy.getHeaderField(HttpHeader.CONTENT_TYPE));
+  }
+
+  @Test
+  public void testMatcherNoHeader() {
+    Header header = new Header(1);
+
+    assertFalse(header.isHeaderMatching(HttpHeader.CONTENT_TYPE, BatchParserCommon.PATTERN_MULTIPART_BOUNDARY));
+  }
+
+//  @Test
+//  public void testMatcherFail() {
+//    Header header = new Header(1);
+//    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED + ";boundary=123", 1);
+//
+//    assertFalse(header.isHeaderMatching(HttpHeader.CONTENT_TYPE, BatchParserCommon.PATTERN_HEADER_LINE));
+//  }
+
+  @Test
+  public void testDuplicatedAddList() {
+    Header header = new Header(1);
+    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1);
+    header.addHeader(HttpHeader.CONTENT_TYPE, Arrays.asList(new String[] { HttpContentType.MULTIPART_MIXED,
+        HttpContentType.APPLICATION_ATOM_SVC }), 2);
+
+    assertEquals(HttpContentType.MULTIPART_MIXED + ", " + HttpContentType.APPLICATION_ATOM_SVC, header
+        .getHeader(HttpHeader.CONTENT_TYPE));
+    assertEquals(2, header.getHeaders(HttpHeader.CONTENT_TYPE).size());
+    assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeaders(HttpHeader.CONTENT_TYPE).get(0));
+    assertEquals(HttpContentType.APPLICATION_ATOM_SVC, header.getHeaders(HttpHeader.CONTENT_TYPE).get(1));
+  }
+
+  @Test
+  public void testRemove() {
+    Header header = new Header(1);
+    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1);
+    header.removeHeader(HttpHeader.CONTENT_TYPE);
+
+    assertNull(header.getHeader(HttpHeader.CONTENT_TYPE));
+    assertEquals(0, header.getHeaders(HttpHeader.CONTENT_TYPE).size());
+  }
+
+  @Test
+  public void testMultipleValues() {
+    Header header = new Header(1);
+    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1);
+    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.APPLICATION_ATOM_SVC, 2);
+    header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.APPLICATION_ATOM_XML, 3);
+
+    final String fullHeaderString =
+        HttpContentType.MULTIPART_MIXED + ", " + HttpContentType.APPLICATION_ATOM_SVC + ", "
+            + HttpContentType.APPLICATION_ATOM_XML;
+
+    assertEquals(fullHeaderString, header.getHeader(HttpHeader.CONTENT_TYPE));
+    assertEquals(3, header.getHeaders(HttpHeader.CONTENT_TYPE).size());
+    assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeaders(HttpHeader.CONTENT_TYPE).get(0));
+    assertEquals(HttpContentType.APPLICATION_ATOM_SVC, header.getHeaders(HttpHeader.CONTENT_TYPE).get(1));
+    assertEquals(HttpContentType.APPLICATION_ATOM_XML, header.getHeaders(HttpHeader.CONTENT_TYPE).get(2));
+  }
+  
+  @Test
+  public void testSplitValues() {
+    final String values = "abc, def,123,77,   99, ysd";
+    List<String> splittedValues = Header.splitValuesByComma(values);
+
+    assertEquals(6, splittedValues.size());
+    assertEquals("abc", splittedValues.get(0));
+    assertEquals("def", splittedValues.get(1));
+    assertEquals("123", splittedValues.get(2));
+    assertEquals("77", splittedValues.get(3));
+    assertEquals("99", splittedValues.get(4));
+    assertEquals("ysd", splittedValues.get(5));
+  }
+}

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/51acf8ae/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/StringUtil.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/StringUtil.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/StringUtil.java
new file mode 100644
index 0000000..8fcebd0
--- /dev/null
+++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/StringUtil.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.olingo.server.core.deserializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+
+import org.apache.olingo.commons.api.ODataRuntimeException;
+import org.apache.olingo.server.core.deserializer.batch.BufferedReaderIncludingLineEndings;
+
+public class StringUtil {
+  
+  
+  public static String toString(final InputStream in) throws IOException {
+    final StringBuilder builder = new StringBuilder();
+    final BufferedReaderIncludingLineEndings reader = new BufferedReaderIncludingLineEndings(new InputStreamReader(in));
+    String currentLine;
+    
+    while((currentLine = reader.readLine()) != null) {
+      builder.append(currentLine);
+    }
+    
+    reader.close();
+    
+    return builder.toString();
+  }
+
+  public static InputStream toInputStream(final String string) {
+    try {
+      return new ByteArrayInputStream(string.getBytes("UTF-8"));
+    } catch (UnsupportedEncodingException e) {
+      throw new ODataRuntimeException("Charset UTF-8 not found");
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/51acf8ae/lib/server-core/src/test/java/org/apache/olingo/server/core/serializer/BatchResponseWriterTest.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/serializer/BatchResponseWriterTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/serializer/BatchResponseWriterTest.java
new file mode 100644
index 0000000..d44f3c3
--- /dev/null
+++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/serializer/BatchResponseWriterTest.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.olingo.server.core.serializer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.olingo.commons.api.http.HttpHeader;
+import org.apache.olingo.commons.api.http.HttpStatusCode;
+import org.apache.olingo.server.api.ODataResponse;
+import org.apache.olingo.server.api.batch.BatchException;
+import org.apache.olingo.server.api.deserializer.batch.ODataResponsePart;
+import org.apache.olingo.server.core.deserializer.StringUtil;
+import org.apache.olingo.server.core.deserializer.batch.BatchParserCommon;
+import org.apache.olingo.server.core.deserializer.batch.BufferedReaderIncludingLineEndings;
+import org.apache.olingo.server.core.serializer.BatchResponseSerializer;
+import org.junit.Test;
+
+public class BatchResponseWriterTest {
+  private static final String CRLF = "\r\n";
+
+  @Test
+  public void testBatchResponse() throws IOException, BatchException {
+    final List<ODataResponsePart> parts = new ArrayList<ODataResponsePart>();
+    ODataResponse response = new ODataResponse();
+    response.setStatusCode(HttpStatusCode.OK.getStatusCode());
+    response.setHeader(HttpHeader.CONTENT_TYPE, "application/json");
+    response.setContent(StringUtil.toInputStream("Walter Winter" + CRLF));
+
+    List<ODataResponse> responses = new ArrayList<ODataResponse>(1);
+    responses.add(response);
+    parts.add(new ODataResponsePart(responses, false));
+
+    ODataResponse changeSetResponse = new ODataResponse();
+    changeSetResponse.setStatusCode(HttpStatusCode.NO_CONTENT.getStatusCode());
+    changeSetResponse.setHeader(BatchParserCommon.HTTP_CONTENT_ID, "1");
+    responses = new ArrayList<ODataResponse>(1);
+    responses.add(changeSetResponse);
+    parts.add(new ODataResponsePart(responses, true));
+
+    BatchResponseSerializer writer = new BatchResponseSerializer();
+    ODataResponse batchResponse = new ODataResponse();
+    writer.toODataResponse(parts, batchResponse);
+
+    assertEquals(202, batchResponse.getStatusCode());
+    assertNotNull(batchResponse.getContent());
+    final BufferedReaderIncludingLineEndings reader =
+        new BufferedReaderIncludingLineEndings(new InputStreamReader(batchResponse.getContent()));
+    final List<String> body = reader.toList();
+    reader.close();
+    
+    int line = 0;
+    assertEquals(25, body.size());
+    assertTrue(body.get(line++).contains("--batch_"));
+    assertEquals("Content-Type: application/http" + CRLF, body.get(line++));
+    assertEquals("Content-Transfer-Encoding: binary" + CRLF, body.get(line++));
+    assertEquals(CRLF, body.get(line++));
+    assertEquals("HTTP/1.1 200 OK" + CRLF, body.get(line++));
+    assertEquals("Content-Type: application/json" + CRLF, body.get(line++));
+    assertEquals("Content-Length: 15" + CRLF, body.get(line++));
+    assertEquals(CRLF, body.get(line++));
+    assertEquals("Walter Winter" + CRLF, body.get(line++));
+    assertEquals(CRLF, body.get(line++));
+    assertTrue(body.get(line++).contains("--batch_"));
+    assertTrue(body.get(line++).contains("Content-Type: multipart/mixed; boundary=changeset_"));
+    assertEquals(CRLF, body.get(line++));
+    assertTrue(body.get(line++).contains("--changeset_"));
+    assertEquals("Content-Type: application/http" + CRLF, body.get(line++));
+    assertEquals("Content-Transfer-Encoding: binary" + CRLF, body.get(line++));
+    assertEquals("Content-Id: 1" + CRLF, body.get(line++));
+    assertEquals(CRLF, body.get(line++));
+    assertEquals("HTTP/1.1 204 No Content" + CRLF, body.get(line++));
+    assertEquals("Content-Length: 0" + CRLF, body.get(line++));
+    assertEquals(CRLF, body.get(line++));
+    assertEquals(CRLF, body.get(line++));
+    assertTrue(body.get(line++).contains("--changeset_"));
+    assertEquals(CRLF, body.get(line++));
+    assertTrue(body.get(line++).contains("--batch_"));
+  }
+
+  @Test
+  public void testResponse() throws IOException, BatchException {
+    List<ODataResponsePart> parts = new ArrayList<ODataResponsePart>();
+    ODataResponse response = new ODataResponse();
+    response.setStatusCode(HttpStatusCode.OK.getStatusCode());
+    response.setHeader(HttpHeader.CONTENT_TYPE, "application/json");
+    response.setContent(StringUtil.toInputStream("Walter Winter"));
+
+    List<ODataResponse> responses = new ArrayList<ODataResponse>(1);
+    responses.add(response);
+    parts.add(new ODataResponsePart(responses, false));
+
+    ODataResponse batchResponse = new ODataResponse();
+    new BatchResponseSerializer().toODataResponse(parts, batchResponse);
+
+    assertEquals(202, batchResponse.getStatusCode());
+    assertNotNull(batchResponse.getContent());
+    final BufferedReaderIncludingLineEndings reader =
+        new BufferedReaderIncludingLineEndings(new InputStreamReader(batchResponse.getContent()));
+    final List<String> body = reader.toList();
+    reader.close();
+    
+    int line = 0;
+    assertEquals(10, body.size());
+    assertTrue(body.get(line++).contains("--batch_"));
+    assertEquals("Content-Type: application/http" + CRLF, body.get(line++));
+    assertEquals("Content-Transfer-Encoding: binary" + CRLF, body.get(line++));
+    assertEquals(CRLF, body.get(line++));
+    assertEquals("HTTP/1.1 200 OK" + CRLF, body.get(line++));
+    assertEquals("Content-Type: application/json" + CRLF, body.get(line++));
+    assertEquals("Content-Length: 13" + CRLF, body.get(line++));
+    assertEquals(CRLF, body.get(line++));
+    assertEquals("Walter Winter" + CRLF, body.get(line++));
+    assertTrue(body.get(line++).contains("--batch_"));
+  }
+
+  @Test
+  public void testChangeSetResponse() throws IOException, BatchException {
+    List<ODataResponsePart> parts = new ArrayList<ODataResponsePart>();
+    ODataResponse response = new ODataResponse();
+    response.setHeader(BatchParserCommon.HTTP_CONTENT_ID, "1");
+    response.setStatusCode(HttpStatusCode.NO_CONTENT.getStatusCode());
+
+    List<ODataResponse> responses = new ArrayList<ODataResponse>(1);
+    responses.add(response);
+    parts.add(new ODataResponsePart(responses, true));
+
+    BatchResponseSerializer writer = new BatchResponseSerializer();
+    ODataResponse batchResponse = new ODataResponse();
+    writer.toODataResponse(parts, batchResponse);
+
+    assertEquals(202, batchResponse.getStatusCode());
+    assertNotNull(batchResponse.getContent());
+
+    final BufferedReaderIncludingLineEndings reader =
+        new BufferedReaderIncludingLineEndings(new InputStreamReader(batchResponse.getContent()));
+    final List<String> body = reader.toList();
+    reader.close();
+    
+    int line = 0;
+    assertEquals(15, body.size());
+    assertTrue(body.get(line++).contains("--batch_"));
+    assertTrue(body.get(line++).contains("Content-Type: multipart/mixed; boundary=changeset_"));
+    assertEquals(CRLF, body.get(line++));
+    assertTrue(body.get(line++).contains("--changeset_"));
+    assertEquals("Content-Type: application/http" + CRLF, body.get(line++));
+    assertEquals("Content-Transfer-Encoding: binary" + CRLF, body.get(line++));
+    assertEquals("Content-Id: 1" + CRLF, body.get(line++));
+    assertEquals(CRLF, body.get(line++));
+    assertEquals("HTTP/1.1 204 No Content" + CRLF, body.get(line++));
+    assertEquals("Content-Length: 0" + CRLF, body.get(line++));
+    assertEquals(CRLF, body.get(line++));
+    assertEquals(CRLF, body.get(line++));
+    assertTrue(body.get(line++).contains("--changeset_"));
+    assertEquals(CRLF, body.get(line++));
+    assertTrue(body.get(line++).contains("--batch_"));
+  }
+}

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/51acf8ae/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/TechnicalServlet.java
----------------------------------------------------------------------
diff --git a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/TechnicalServlet.java b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/TechnicalServlet.java
index 78c9caa..7fa981b 100644
--- a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/TechnicalServlet.java
+++ b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/TechnicalServlet.java
@@ -18,29 +18,30 @@
  */
 package org.apache.olingo.server.tecsvc;
 
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
 import org.apache.olingo.server.api.OData;
 import org.apache.olingo.server.api.ODataHttpHandler;
 import org.apache.olingo.server.api.ServiceMetadata;
 import org.apache.olingo.server.api.edmx.EdmxReference;
 import org.apache.olingo.server.api.edmx.EdmxReferenceInclude;
 import org.apache.olingo.server.tecsvc.data.DataProvider;
+import org.apache.olingo.server.tecsvc.processor.TechnicalBatchProcessor;
 import org.apache.olingo.server.tecsvc.processor.TechnicalEntityProcessor;
 import org.apache.olingo.server.tecsvc.processor.TechnicalPrimitiveComplexProcessor;
 import org.apache.olingo.server.tecsvc.provider.EdmTechProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-
-import java.io.IOException;
-import java.net.URI;
-import java.util.Arrays;
-import java.util.List;
-
 public class TechnicalServlet extends HttpServlet {
 
   private static final long serialVersionUID = 1L;
@@ -67,6 +68,7 @@ public class TechnicalServlet extends HttpServlet {
       ODataHttpHandler handler = odata.createHandler(serviceMetadata);
       handler.register(new TechnicalEntityProcessor(dataProvider));
       handler.register(new TechnicalPrimitiveComplexProcessor(dataProvider));
+      handler.register(new TechnicalBatchProcessor(dataProvider));
       handler.process(req, resp);
     } catch (RuntimeException e) {
       LOG.error("Server Error", e);