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 16:12:09 UTC

[08/22] olingo-odata4 git commit: MockedBatchHandler Test

MockedBatchHandler Test

Signed-off-by: Christian Amend <ch...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/olingo-odata4/repo
Commit: http://git-wip-us.apache.org/repos/asf/olingo-odata4/commit/0bd32951
Tree: http://git-wip-us.apache.org/repos/asf/olingo-odata4/tree/0bd32951
Diff: http://git-wip-us.apache.org/repos/asf/olingo-odata4/diff/0bd32951

Branch: refs/heads/master
Commit: 0bd32951b8ebcfce895fa5d5eb8fd76954d6bfd3
Parents: ee2451c
Author: Christian Holzer <c....@sap.com>
Authored: Mon Nov 3 17:16:47 2014 +0100
Committer: Christian Amend <ch...@apache.org>
Committed: Thu Nov 13 17:10:56 2014 +0100

----------------------------------------------------------------------
 .../olingo/server/api/batch/BatchOperation.java |   2 +-
 .../server/api/processor/BatchProcessor.java    |   2 +-
 .../batch/handler/BatchChangeSetSorter.java     |  41 +-
 .../core/batch/handler/BatchOperationImpl.java  |   2 +-
 .../core/batch/handler/BatchPartHandler.java    |  56 +-
 .../BatchRequestTransformator.java              |  17 +-
 .../transformator/BatchTransformatorCommon.java | 159 -----
 .../transformator/HttpRequestStatusLine.java    | 229 ++++++++
 .../batch/handler/BatchChangeSetSorterTest.java |   4 +-
 .../batch/handler/MockedBatchHandlerTest.java   | 582 +++++++++++++++++++
 10 files changed, 894 insertions(+), 200 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/0bd32951/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchOperation.java
----------------------------------------------------------------------
diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchOperation.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchOperation.java
index af7d9c1..2d09a94 100644
--- a/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchOperation.java
+++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchOperation.java
@@ -27,7 +27,7 @@ import org.apache.olingo.server.api.ODataResponse;
 public interface BatchOperation {
   public List<BatchRequestPart> parseBatchRequest(InputStream in) throws BatchException;
 
-  public ODataResponse handleODataRequest(ODataRequest request, BatchRequestPart requestPart);
+  public ODataResponse handleODataRequest(ODataRequest request, BatchRequestPart requestPart) throws BatchException;
 
   public ODataResponsePart handleBatchRequest(BatchRequestPart request) throws BatchException;
 

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/0bd32951/lib/server-api/src/main/java/org/apache/olingo/server/api/processor/BatchProcessor.java
----------------------------------------------------------------------
diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/processor/BatchProcessor.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/processor/BatchProcessor.java
index 221b38a..a0a7135 100644
--- a/lib/server-api/src/main/java/org/apache/olingo/server/api/processor/BatchProcessor.java
+++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/processor/BatchProcessor.java
@@ -26,7 +26,7 @@ import org.apache.olingo.server.api.batch.BatchOperation;
 import org.apache.olingo.server.api.batch.BatchRequestPart;
 
 public interface BatchProcessor extends Processor {
-  void executeBatch(BatchOperation operation, ODataRequest requests, ODataResponse response);
+  void executeBatch(BatchOperation operation, ODataRequest request, ODataResponse response);
 
   List<ODataResponse> executeChangeSet(BatchOperation operation, List<ODataRequest> requests,
       BatchRequestPart requestPart);

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/0bd32951/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchChangeSetSorter.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchChangeSetSorter.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchChangeSetSorter.java
index c348512..f8ac653 100644
--- a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchChangeSetSorter.java
+++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchChangeSetSorter.java
@@ -39,7 +39,7 @@ public class BatchChangeSetSorter {
 
   private static Pattern referencePattern = Pattern.compile(REG_EX_REFERENCE);
   private Set<String> knownContentId = new HashSet<String>();
-  private Map<String, List<ODataRequest>> requestContentIdMapping = new HashMap<String, List<ODataRequest>>();
+  private Map<String, List<ODataRequest>> requestReferenceMapping = new HashMap<String, List<ODataRequest>>();
 
   public BatchChangeSetSorter(List<ODataRequest> requests) throws BatchException {
     sort(requests);
@@ -50,9 +50,12 @@ public class BatchChangeSetSorter {
   }
 
   private List<ODataRequest> sort(final List<ODataRequest> requests) throws BatchException {
-    extractUrlContentId(requests);
-    orderdList.addAll(getRequestsWithoutReferences());
-
+    extractUrlReference(requests);
+    
+    final List<ODataRequest> requestsWithoutReferences = getRequestsWithoutReferences();
+    orderdList.addAll(requestsWithoutReferences);
+    addRequestsToKnownContentIds(requestsWithoutReferences);
+    
     boolean areRequestsProcessed = true;
     while (requestsToProcessAvailable() && areRequestsProcessed) {
       areRequestsProcessed = processRemainingRequests(orderdList);
@@ -66,7 +69,7 @@ public class BatchChangeSetSorter {
   }
 
   private boolean requestsToProcessAvailable() {
-    return requestContentIdMapping.keySet().size() != 0;
+    return requestReferenceMapping.keySet().size() != 0;
   }
 
   private boolean processRemainingRequests(List<ODataRequest> orderdList) {
@@ -81,10 +84,10 @@ public class BatchChangeSetSorter {
     List<ODataRequest> result = new ArrayList<ODataRequest>();
 
     for (String contextId : knownContentId) {
-      List<ODataRequest> processedRequests = requestContentIdMapping.get(contextId);
+      List<ODataRequest> processedRequests = requestReferenceMapping.get(contextId);
       if (processedRequests != null && processedRequests.size() != 0) {
         result.addAll(processedRequests);
-        requestContentIdMapping.remove(contextId);
+        requestReferenceMapping.remove(contextId);
       }
     }
 
@@ -92,10 +95,9 @@ public class BatchChangeSetSorter {
   }
 
   private List<ODataRequest> getRequestsWithoutReferences() {
-    final List<ODataRequest> requestsWithoutReference = requestContentIdMapping.get(null);
-    requestContentIdMapping.remove(null);
+    final List<ODataRequest> requestsWithoutReference = requestReferenceMapping.get(null);
+    requestReferenceMapping.remove(null);
 
-    addRequestsToKnownContentIds(requestsWithoutReference);
     return requestsWithoutReference;
   }
 
@@ -112,33 +114,38 @@ public class BatchChangeSetSorter {
     return request.getHeader(BatchParserCommon.HTTP_CONTENT_ID);
   }
 
-  private void extractUrlContentId(List<ODataRequest> requests) {
+  private void extractUrlReference(List<ODataRequest> requests) {
     for (ODataRequest request : requests) {
       final String reference = getReferenceInURI(request);
-      addRequestToMapping(reference, request);
+      addRequestToReferenceMapping(reference, request);
     }
   }
 
-  private void addRequestToMapping(final String reference, final ODataRequest request) {
-    List<ODataRequest> requestList = requestContentIdMapping.get(reference);
+  private void addRequestToReferenceMapping(final String reference, final ODataRequest request) {
+    List<ODataRequest> requestList = requestReferenceMapping.get(reference);
     requestList = (requestList == null) ? new ArrayList<ODataRequest>() : requestList;
 
     requestList.add(request);
-    requestContentIdMapping.put(reference, requestList);
+    requestReferenceMapping.put(reference, requestList);
   }
 
   public static String getReferenceInURI(ODataRequest request) {
-    Matcher matcher = referencePattern.matcher(removeFollingPathSegments(request.getRawODataPath()));
+    Matcher matcher = referencePattern.matcher(removeFollingPathSegments(removeFirstSplash(request.getRawODataPath())));
     return (matcher.matches()) ? matcher.group(1) : null;
   }
 
+  private static String removeFirstSplash(String rawODataPath) {
+    final int indexOfSlash = rawODataPath.indexOf("/");
+    return (indexOfSlash == 0) ? rawODataPath.substring(1) : rawODataPath;
+  }
+
   private static String removeFollingPathSegments(String rawODataPath) {
     final int indexOfSlash = rawODataPath.indexOf("/");
     return (indexOfSlash != -1) ? rawODataPath.substring(0, indexOfSlash) : rawODataPath;
   }
 
   public static void replaceContentIdReference(ODataRequest request, String contentId, String resourceUri) {
-    final String newUri = request.getRawODataPath().replace("$" + contentId, resourceUri);
+    final String newUri = request.getRawODataPath().replace("/$" + contentId, resourceUri);
     request.setRawODataPath(newUri);
     request.setRawRequestUri(request.getRawBaseUri() + "/" + newUri);
   }

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/0bd32951/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchOperationImpl.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchOperationImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchOperationImpl.java
index 1f5e243..cf7ad7c 100644
--- a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchOperationImpl.java
+++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchOperationImpl.java
@@ -53,7 +53,7 @@ public class BatchOperationImpl implements BatchOperation {
   }
   
   @Override
-  public ODataResponse handleODataRequest(ODataRequest request, BatchRequestPart requestPart) {
+  public ODataResponse handleODataRequest(ODataRequest request, BatchRequestPart requestPart) throws BatchException {
     return partHandler.handleODataRequest(request, requestPart);
   }
   

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/0bd32951/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchPartHandler.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchPartHandler.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchPartHandler.java
index df23c55..c678355 100644
--- a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchPartHandler.java
+++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/handler/BatchPartHandler.java
@@ -23,7 +23,9 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.olingo.commons.api.ODataRuntimeException;
 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.ODataResponse;
 import org.apache.olingo.server.api.batch.BatchException;
@@ -33,6 +35,7 @@ import org.apache.olingo.server.api.batch.ODataResponsePart;
 import org.apache.olingo.server.api.processor.BatchProcessor;
 import org.apache.olingo.server.core.ODataHandler;
 import org.apache.olingo.server.core.batch.parser.BatchParserCommon;
+import org.apache.olingo.server.core.batch.transformator.HttpRequestStatusLine.ODataURI;
 
 public class BatchPartHandler {
 
@@ -48,21 +51,18 @@ public class BatchPartHandler {
     this.batchOperation = batchOperation;
   }
 
-  public ODataResponse handleODataRequest(ODataRequest request, BatchRequestPart requestPart) {
+  public ODataResponse handleODataRequest(ODataRequest request, BatchRequestPart requestPart) throws BatchException {
     final ODataResponse response;
     
     if(requestPart.isChangeSet()) {
-      final UriMapping mapping = getUriMappingOrDefault(requestPart);
-      final String reference = BatchChangeSetSorter.getReferenceInURI(request);
-      if(reference != null) {
-        BatchChangeSetSorter.replaceContentIdReference(request, reference, mapping.getUri(reference));
-      }
-      
-       response = oDataHandler.process(request);
+      final UriMapping mapping = replaceReference(request, requestPart);
+
+      response = oDataHandler.process(request);
        
+      final String resourceUri = getODataPath(request, response);
       final String contentId = request.getHeader(BatchParserCommon.HTTP_CONTENT_ID);
-      final String locationHeader = request.getHeader(HttpHeader.LOCATION);
-      mapping.addMapping(contentId, locationHeader);
+
+      mapping.addMapping(contentId, resourceUri);
     } else {
       response = oDataHandler.process(request);
     }
@@ -74,11 +74,45 @@ public class BatchPartHandler {
     
     return  response;
   }
+
+  private String getODataPath(ODataRequest request, ODataResponse response) throws BatchException {
+    String resourceUri = null;
+    
+    if(request.getMethod() == HttpMethod.POST) {
+      // Create entity
+      // The URI of the new resource will be generated by the server and published in the location header
+      ODataURI uri = new ODataURI(response.getHeaders().get(HttpHeader.LOCATION), request.getRawBaseUri());
+      resourceUri = uri.getRawODataPath();
+    } else {
+      // Update, Upsert (PUT, PATCH, Delete)
+      // These methods still addresses a given URI, so we use the URI given by the request
+      resourceUri = request.getRawODataPath();
+    }
+    
+    return resourceUri;
+  }
+
+  private UriMapping replaceReference(ODataRequest request, BatchRequestPart requestPart) {
+    final UriMapping mapping = getUriMappingOrDefault(requestPart);
+    final String reference = BatchChangeSetSorter.getReferenceInURI(request);
+    
+    if(reference != null) {
+      final String replacement = mapping.getUri(reference);
+      
+      if(replacement != null) {
+        BatchChangeSetSorter.replaceContentIdReference(request, reference, replacement);
+      } else {
+        throw new ODataRuntimeException("Required Content-Id for reference \"" + reference + "\" not found.");
+      }
+    }
+    
+    return mapping;
+  }
   
   private UriMapping getUriMappingOrDefault(final BatchRequestPart requestPart) {
     UriMapping mapping = uriMapping.get(requestPart);
     
-    if(uriMapping == null) {
+    if(mapping == null) {
       mapping = new UriMapping();
     }
     uriMapping.put(requestPart, mapping);

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/0bd32951/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchRequestTransformator.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchRequestTransformator.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchRequestTransformator.java
index 94d5226..0bf0104 100644
--- a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchRequestTransformator.java
+++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchRequestTransformator.java
@@ -38,7 +38,7 @@ import org.apache.olingo.server.core.batch.parser.BatchQueryOperation;
 import org.apache.olingo.server.core.batch.parser.BatchRequestPartImpl;
 import org.apache.olingo.server.core.batch.parser.Header;
 import org.apache.olingo.server.core.batch.parser.HeaderField;
-import org.apache.olingo.server.core.batch.transformator.BatchTransformatorCommon.HttpRequestStatusLine;
+import org.apache.olingo.server.core.batch.transformator.HttpRequestStatusLine.ODataURI;
 
 public class BatchRequestTransformator implements BatchTransformator {
   private final String baseUri;
@@ -111,20 +111,21 @@ public class BatchRequestTransformator implements BatchTransformator {
         new HttpRequestStatusLine(operation.getHttpStatusLine(), baseUri, rawServiceResolutionUri, operation
             .getHeaders());
     statusLine.validateHttpMethod(isChangeSet);
-
+    final ODataURI uri = statusLine.getUri();
+    
     validateBody(statusLine, operation);
     InputStream bodyStrean = getBodyStream(operation, statusLine);
 
     validateForbiddenHeader(operation);
-
+    
     final ODataRequest request = new ODataRequest();
     request.setBody(bodyStrean);
     request.setMethod(statusLine.getMethod());
-    request.setRawBaseUri(statusLine.getRawBaseUri());
-    request.setRawODataPath(statusLine.getRawODataPath());
-    request.setRawQueryPath(statusLine.getRawQueryPath());
-    request.setRawRequestUri(statusLine.getRawRequestUri());
-    request.setRawServiceResolutionUri(statusLine.getRawServiceResolutionUri());
+    request.setRawBaseUri(uri.getRawBaseUri());
+    request.setRawODataPath(uri.getRawODataPath());
+    request.setRawQueryPath(uri.getRawQueryPath());
+    request.setRawRequestUri(uri.getRawRequestUri());
+    request.setRawServiceResolutionUri(uri.getRawServiceResolutionUri());
 
     for (final HeaderField field : operation.getHeaders()) {
       request.addHeader(field.getFieldName(), field.getValues());

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/0bd32951/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformatorCommon.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformatorCommon.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformatorCommon.java
index c9da563..fc2035a 100644
--- a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformatorCommon.java
+++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformatorCommon.java
@@ -18,20 +18,14 @@
  */
 package org.apache.olingo.server.core.batch.transformator;
 
-import java.util.Arrays;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.apache.olingo.commons.api.http.HttpContentType;
 import org.apache.olingo.commons.api.http.HttpHeader;
-import org.apache.olingo.commons.api.http.HttpMethod;
 import org.apache.olingo.server.api.batch.BatchException;
 import org.apache.olingo.server.api.batch.BatchException.MessageKeys;
 import org.apache.olingo.server.core.batch.parser.BatchParserCommon;
-import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings.Line;
 import org.apache.olingo.server.core.batch.parser.Header;
 import org.apache.olingo.server.core.batch.parser.HeaderField;
 
@@ -94,157 +88,4 @@ public class BatchTransformatorCommon {
 
     return -1;
   }
-
-  public static class HttpRequestStatusLine {
-    private static final Pattern PATTERN_RELATIVE_URI = Pattern.compile("([^/][^?]*)(?:\\?(.*))?");
-    private static final Pattern PATTERN_ABSOLUTE_URI_WITH_HOST = Pattern.compile("(/[^?]*)(?:\\?(.*))?");
-    private static final Pattern PATTERN_ABSOLUTE_URI = Pattern.compile("(http[s]?://[^?]*)(?:\\?(.*))?");
-
-    private static final Set<String> HTTP_BATCH_METHODS = new HashSet<String>(Arrays.asList(new String[] { "GET" }));
-    private static final Set<String> HTTP_CHANGE_SET_METHODS = new HashSet<String>(Arrays.asList(new String[] { "POST",
-        "PUT", "DELETE", "MERGE", "PATCH" }));
-    private static final String HTTP_VERSION = "HTTP/1.1";
-
-    final private Line statusLine;
-    final String requestBaseUri;
-
-    private HttpMethod method;
-    private String httpVersion;
-    private String rawServiceResolutionUri;
-    private String rawQueryPath;
-    private String rawODataPath;
-    private String rawBaseUri;
-    private Header header;
-    private String rawRequestUri;
-
-    public HttpRequestStatusLine(final Line httpStatusLine, final String baseUri, final String serviceResolutionUri,
-        final Header requestHeader)
-        throws BatchException {
-      statusLine = httpStatusLine;
-      requestBaseUri = baseUri;
-      header = requestHeader;
-      rawServiceResolutionUri = serviceResolutionUri;
-      
-      parse();
-    }
-
-    private void parse() throws BatchException {
-      final String[] parts = statusLine.toString().split(" ");
-
-      if (parts.length == 3) {
-        method = parseMethod(parts[0]);
-        parseRawPaths(parts[1]);
-        httpVersion = parseHttpVersion(parts[2]);
-      } else {
-        throw new BatchException("Invalid status line", MessageKeys.INVALID_STATUS_LINE, statusLine.getLineNumber());
-      }
-    }
-
-    private HttpMethod parseMethod(final String method) throws BatchException {
-      try {
-        return HttpMethod.valueOf(method.trim());
-      } catch (IllegalArgumentException e) {
-        throw new BatchException("Illegal http method", MessageKeys.INVALID_METHOD, statusLine.getLineNumber());
-      }
-    }
-
-    private void parseRawPaths(final String rawUrl) throws BatchException {
-      final Matcher absoluteUriMatcher = PATTERN_ABSOLUTE_URI.matcher(rawUrl);
-      final Matcher absoluteUriWtithHostMatcher = PATTERN_ABSOLUTE_URI_WITH_HOST.matcher(rawUrl);
-      final Matcher relativeUriMatcher = PATTERN_RELATIVE_URI.matcher(rawUrl);
-
-      if (absoluteUriMatcher.matches()) {
-        buildUri(absoluteUriMatcher.group(1), absoluteUriMatcher.group(2));
-
-      } else if (absoluteUriWtithHostMatcher.matches()) {
-        final List<String> hostHeader = header.getHeaders(HttpHeader.HOST);
-        if (hostHeader.size() == 1) {
-          buildUri(hostHeader.get(0) + absoluteUriWtithHostMatcher.group(1), absoluteUriWtithHostMatcher.group(2));
-        } else {
-          throw new BatchException("Exactly one host header is required", MessageKeys.MISSING_MANDATORY_HEADER,
-              statusLine.getLineNumber());
-        }
-
-      } else if (relativeUriMatcher.matches()) {
-        buildUri(requestBaseUri + "/" + relativeUriMatcher.group(1), relativeUriMatcher.group(2));
-
-      } else {
-        throw new BatchException("Invalid uri", MessageKeys.INVALID_URI, statusLine.getLineNumber());
-      }
-    }
-
-    private void buildUri(final String resourceUri, final String queryOptions) throws BatchException {
-      if(!resourceUri.startsWith(requestBaseUri)) {
-        throw new BatchException("Host do not match", MessageKeys.INVALID_URI, statusLine.getLineNumber());
-      }
-      
-      final int oDataPathIndex = resourceUri.indexOf(requestBaseUri);
-
-      rawBaseUri = requestBaseUri;
-      rawODataPath = resourceUri.substring(oDataPathIndex + requestBaseUri.length());
-      rawServiceResolutionUri = "";
-      rawRequestUri = requestBaseUri + rawODataPath;
-
-      if (queryOptions != null) {
-        rawRequestUri += "?" + queryOptions;
-        rawQueryPath = queryOptions;
-      } else {
-        rawQueryPath = "";
-      }
-    }
-
-    private String parseHttpVersion(final String httpVersion) throws BatchException {
-      if (!HTTP_VERSION.equals(httpVersion.trim())) {
-        throw new BatchException("Invalid http version", MessageKeys.INVALID_HTTP_VERSION, statusLine.getLineNumber());
-      } else {
-        return HTTP_VERSION;
-      }
-    }
-
-    public void validateHttpMethod(boolean isChangeSet) throws BatchException {
-      Set<String> validMethods = (isChangeSet) ? HTTP_CHANGE_SET_METHODS : HTTP_BATCH_METHODS;
-
-      if (!validMethods.contains(getMethod().toString())) {
-        if (isChangeSet) {
-          throw new BatchException("Invalid change set method", MessageKeys.INVALID_CHANGESET_METHOD, statusLine
-              .getLineNumber());
-        } else {
-          throw new BatchException("Invalid query operation method", MessageKeys.INVALID_QUERY_OPERATION_METHOD,
-              statusLine.getLineNumber());
-        }
-      }
-    }
-
-    public HttpMethod getMethod() {
-      return method;
-    }
-
-    public String getHttpVersion() {
-      return httpVersion;
-    }
-
-    public int getLineNumber() {
-      return statusLine.getLineNumber();
-    }
-
-    public String getRawBaseUri() {
-      return rawBaseUri;
-    }
-
-    public String getRawODataPath() {
-      return rawODataPath;
-    }
-
-    public String getRawQueryPath() {
-      return rawQueryPath;
-    }
-
-    public String getRawRequestUri() {
-      return rawRequestUri;
-    }
-
-    public String getRawServiceResolutionUri() {
-      return rawServiceResolutionUri;
-    }
-  }
 }

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/0bd32951/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/HttpRequestStatusLine.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/HttpRequestStatusLine.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/HttpRequestStatusLine.java
new file mode 100644
index 0000000..416d593
--- /dev/null
+++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/HttpRequestStatusLine.java
@@ -0,0 +1,229 @@
+/*
+ * 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.batch.transformator;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.olingo.commons.api.http.HttpHeader;
+import org.apache.olingo.commons.api.http.HttpMethod;
+import org.apache.olingo.server.api.batch.BatchException;
+import org.apache.olingo.server.api.batch.BatchException.MessageKeys;
+import org.apache.olingo.server.core.batch.parser.Header;
+import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings.Line;
+
+public class HttpRequestStatusLine {
+  private static final Pattern PATTERN_RELATIVE_URI = Pattern.compile("([^/][^?]*)(?:\\?(.*))?");
+  private static final Pattern PATTERN_ABSOLUTE_URI_WITH_HOST = Pattern.compile("(/[^?]*)(?:\\?(.*))?");
+  private static final Pattern PATTERN_ABSOLUTE_URI = Pattern.compile("(http[s]?://[^?]*)(?:\\?(.*))?");
+
+  private static final Set<String> HTTP_BATCH_METHODS = new HashSet<String>(Arrays.asList(new String[] { "GET" }));
+  private static final Set<String> HTTP_CHANGE_SET_METHODS = new HashSet<String>(Arrays.asList(new String[] { "POST",
+      "PUT", "DELETE", "MERGE", "PATCH" }));
+  // TODO Is Merge still supported?
+  // What`s New in OData 4: 2.7.2 Pruned: MERGE
+  // MERGE was used to do PATCH before PATCH existed. Now that we have PATCH, we no longer need MERGE. => No
+  private static final String HTTP_VERSION = "HTTP/1.1";
+
+  final private Line statusLine;
+  final String requestBaseUri;
+
+  private HttpMethod method;
+  private String httpVersion;
+  private Header header;
+  private ODataURI uri;
+
+  public HttpRequestStatusLine(final Line httpStatusLine, final String baseUri, final String serviceResolutionUri,
+      final Header requestHeader)
+      throws BatchException {
+    statusLine = httpStatusLine;
+    requestBaseUri = baseUri;
+    header = requestHeader;
+
+    parse();
+  }
+
+  private void parse() throws BatchException {
+    final String[] parts = statusLine.toString().split(" ");
+
+    if (parts.length == 3) {
+      method = parseMethod(parts[0]);
+      uri = new ODataURI(parts[1], requestBaseUri, statusLine.getLineNumber(), header.getHeaders(HttpHeader.HOST));
+      httpVersion = parseHttpVersion(parts[2]);
+    } else {
+      throw new BatchException("Invalid status line", MessageKeys.INVALID_STATUS_LINE, statusLine.getLineNumber());
+    }
+  }
+
+  private HttpMethod parseMethod(final String method) throws BatchException {
+    try {
+      return HttpMethod.valueOf(method.trim());
+    } catch (IllegalArgumentException e) {
+      throw new BatchException("Illegal http method", MessageKeys.INVALID_METHOD, statusLine.getLineNumber());
+    }
+  }
+
+  private String parseHttpVersion(final String httpVersion) throws BatchException {
+    if (!HTTP_VERSION.equals(httpVersion.trim())) {
+      throw new BatchException("Invalid http version", MessageKeys.INVALID_HTTP_VERSION, statusLine.getLineNumber());
+    } else {
+      return HTTP_VERSION;
+    }
+  }
+
+  public void validateHttpMethod(boolean isChangeSet) throws BatchException {
+    Set<String> validMethods = (isChangeSet) ? HTTP_CHANGE_SET_METHODS : HTTP_BATCH_METHODS;
+
+    if (!validMethods.contains(getMethod().toString())) {
+      if (isChangeSet) {
+        throw new BatchException("Invalid change set method", MessageKeys.INVALID_CHANGESET_METHOD, statusLine
+            .getLineNumber());
+      } else {
+        throw new BatchException("Invalid query operation method", MessageKeys.INVALID_QUERY_OPERATION_METHOD,
+            statusLine.getLineNumber());
+      }
+    }
+  }
+
+  public HttpMethod getMethod() {
+    return method;
+  }
+
+  public String getHttpVersion() {
+    return httpVersion;
+  }
+
+  public int getLineNumber() {
+    return statusLine.getLineNumber();
+  }
+
+  public ODataURI getUri() {
+    return uri;
+  }
+
+  public static class ODataURI {
+    private String rawServiceResolutionUri;
+    private String rawQueryPath;
+    private String rawODataPath;
+    private String rawBaseUri;
+    private String rawRequestUri;
+    private final String requestBaseUri;
+    private final int lineNumber;
+
+    public ODataURI(final String rawUri, String requestBaseUri) throws BatchException {
+      this(rawUri, requestBaseUri, 0, new ArrayList<String>());
+    }
+
+    public ODataURI(final String rawUri, String requestBaseUri, int lineNumber, List<String> hostHeader)
+        throws BatchException {
+      this.lineNumber = lineNumber;
+      this.requestBaseUri = requestBaseUri;
+
+      final Matcher absoluteUriMatcher = PATTERN_ABSOLUTE_URI.matcher(rawUri);
+      final Matcher absoluteUriWtithHostMatcher = PATTERN_ABSOLUTE_URI_WITH_HOST.matcher(rawUri);
+      final Matcher relativeUriMatcher = PATTERN_RELATIVE_URI.matcher(rawUri);
+
+      if (absoluteUriMatcher.matches()) {
+        buildUri(absoluteUriMatcher.group(1), absoluteUriMatcher.group(2));
+
+      } else if (absoluteUriWtithHostMatcher.matches()) {
+        if (hostHeader != null && hostHeader.size() == 1) {
+          buildUri(hostHeader.get(0) + absoluteUriWtithHostMatcher.group(1), absoluteUriWtithHostMatcher.group(2));
+        } else {
+          throw new BatchException("Exactly one host header is required", MessageKeys.MISSING_MANDATORY_HEADER,
+              lineNumber);
+        }
+
+      } else if (relativeUriMatcher.matches()) {
+        buildUri(requestBaseUri + "/" + relativeUriMatcher.group(1), relativeUriMatcher.group(2));
+
+      } else {
+        throw new BatchException("Invalid uri", MessageKeys.INVALID_URI, lineNumber);
+      }
+    }
+
+    private void buildUri(final String resourceUri, final String queryOptions) throws BatchException {
+      if (!resourceUri.startsWith(requestBaseUri)) {
+        throw new BatchException("Host do not match", MessageKeys.INVALID_URI, lineNumber);
+      }
+
+      final int oDataPathIndex = resourceUri.indexOf(requestBaseUri);
+
+      rawBaseUri = requestBaseUri;
+      rawODataPath = resourceUri.substring(oDataPathIndex + requestBaseUri.length());
+      rawRequestUri = requestBaseUri + rawODataPath;
+
+      if (queryOptions != null) {
+        rawRequestUri += "?" + queryOptions;
+        rawQueryPath = queryOptions;
+      } else {
+        rawQueryPath = "";
+      }
+    }
+
+    public String getRawServiceResolutionUri() {
+      return rawServiceResolutionUri;
+    }
+
+    public void setRawServiceResolutionUri(String rawServiceResolutionUri) {
+      this.rawServiceResolutionUri = rawServiceResolutionUri;
+    }
+
+    public String getRawQueryPath() {
+      return rawQueryPath;
+    }
+
+    public void setRawQueryPath(String rawQueryPath) {
+      this.rawQueryPath = rawQueryPath;
+    }
+
+    public String getRawODataPath() {
+      return rawODataPath;
+    }
+
+    public void setRawODataPath(String rawODataPath) {
+      this.rawODataPath = rawODataPath;
+    }
+
+    public String getRawBaseUri() {
+      return rawBaseUri;
+    }
+
+    public void setRawBaseUri(String rawBaseUri) {
+      this.rawBaseUri = rawBaseUri;
+    }
+
+    public String getRawRequestUri() {
+      return rawRequestUri;
+    }
+
+    public void setRawRequestUri(String rawRequestUri) {
+      this.rawRequestUri = rawRequestUri;
+    }
+
+    public String getRequestBaseUri() {
+      return requestBaseUri;
+    }
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/0bd32951/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/handler/BatchChangeSetSorterTest.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/handler/BatchChangeSetSorterTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/handler/BatchChangeSetSorterTest.java
index 9196514..c8b725a 100644
--- a/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/handler/BatchChangeSetSorterTest.java
+++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/handler/BatchChangeSetSorterTest.java
@@ -39,7 +39,7 @@ public class BatchChangeSetSorterTest {
   public void test() throws BatchException {
     final List<ODataRequest> changeSet = new ArrayList<ODataRequest>();
     changeSet.add(createRequest(HttpMethod.POST, BASE_URI, "$1/Adress", "2"));
-    changeSet.add(createRequest(HttpMethod.POST, BASE_URI, "Employees", "1"));
+    changeSet.add(createRequest(HttpMethod.POST, BASE_URI, "/Employees", "1"));
     
    BatchChangeSetSorter sorter = new BatchChangeSetSorter(changeSet);
    final List<ODataRequest> sortedChangeSet = sorter.getOrderdRequests();
@@ -110,7 +110,7 @@ public class BatchChangeSetSorterTest {
   @Test
   public void testRewriting() {
     final String CONTENT_ID = "1";
-    final String ODATA_PATH ="$" + CONTENT_ID + "/Address";
+    final String ODATA_PATH ="/$" + CONTENT_ID + "/Address";
     final String RESOURCE_URI = "Employee('1')";
     final ODataRequest request = createRequest(HttpMethod.POST, BASE_URI, ODATA_PATH);
     

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/0bd32951/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/handler/MockedBatchHandlerTest.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/handler/MockedBatchHandlerTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/handler/MockedBatchHandlerTest.java
new file mode 100644
index 0000000..ac111e0
--- /dev/null
+++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/handler/MockedBatchHandlerTest.java
@@ -0,0 +1,582 @@
+/*
+ * 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.batch.handler;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.olingo.commons.api.ODataRuntimeException;
+import org.apache.olingo.commons.api.http.HttpHeader;
+import org.apache.olingo.commons.api.http.HttpMethod;
+import org.apache.olingo.commons.api.http.HttpStatusCode;
+import org.apache.olingo.server.api.OData;
+import org.apache.olingo.server.api.ODataRequest;
+import org.apache.olingo.server.api.ODataResponse;
+import org.apache.olingo.server.api.ServiceMetadata;
+import org.apache.olingo.server.api.batch.BatchException;
+import org.apache.olingo.server.api.batch.BatchOperation;
+import org.apache.olingo.server.api.batch.BatchRequestPart;
+import org.apache.olingo.server.api.batch.ODataResponsePart;
+import org.apache.olingo.server.api.processor.BatchProcessor;
+import org.apache.olingo.server.core.ODataHandler;
+import org.apache.olingo.server.core.batch.parser.BatchParserCommon;
+import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class MockedBatchHandlerTest {
+
+  private static final String BATCH_CONTENT_TYPE = "multipart/mixed;boundary=batch_12345";
+  private static final String BATCH_ODATA_PATH = "/$batch";
+  private static final String BATCH_REQUEST_URI = "http://localhost:8080/odata/$batch";
+  private static final String BASE_URI = "http://localhost:8080/odata";
+  private static final String CRLF = "\r\n";
+  private ODataHandler handler;
+  private int entityCounter = 1;
+
+  @Test
+  public void test() throws BatchException, IOException {
+    final String content = "--batch_12345" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_12345" + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 4" + CRLF
+        + CRLF
+        + "PUT /$3/PropertyInt32 HTTP/1.1" + CRLF // Absolute URI with separate Host header and ref.
+        + "Host: http://localhost:8080/odata" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 5" + CRLF
+        + CRLF
+        + "POST http://localhost:8080/odata/$1/NavPropertyETTwoPrimMany HTTP/1.1" + CRLF // Absolute URI with ref.
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 2" + CRLF
+        + CRLF
+        + "POST $1/NavPropertyETTwoPrimMany HTTP/1.1" + CRLF // Relative URI with ref.
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "POST http://localhost:8080/odata/ESAllPrim HTTP/1.1" + CRLF // Absolute URI
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 3" + CRLF
+        + CRLF
+        + "PUT ESAllPrim(1) HTTP/1.1" + CRLF // Relative URI
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 6" + CRLF
+        + CRLF
+        + "PUT /ESAllPrim(1) HTTP/1.1" + CRLF // Absolute URI with separate Host header
+        + "Host: http://localhost:8080/odata"
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345--" + CRLF
+        + CRLF
+        + "--batch_12345--";
+    final Map<String, List<String>> header = getMimeHeader();
+    final ODataResponse response = new ODataResponse();
+    final BatchHandler batchHandler = buildBatchHandler(content, header);
+
+    batchHandler.process(response);
+
+    BufferedReaderIncludingLineEndings reader =
+        new BufferedReaderIncludingLineEndings(new InputStreamReader(response.getContent()));
+
+    final List<String> responseContent = reader.toList();
+    reader.close();
+
+    int line = 0;
+    assertEquals(63, responseContent.size());
+
+    // Check change set
+    assertTrue(responseContent.get(line++).contains("--batch_"));
+    assertTrue(responseContent.get(line++).contains("Content-Type: multipart/mixed; boundary=changeset_"));
+
+    for (int i = 0; i < 6; i++) {
+      String contentId = checkChangeSetPartHeader(responseContent, line);
+      line += 6;
+
+      if ("1".equals(contentId)) {
+        assertEquals("HTTP/1.1 201 Created" + CRLF, responseContent.get(line++));
+        assertEquals("Location: " + BASE_URI + "/ESAllPrim(1)" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else if ("2".equals(contentId)) {
+        assertEquals("HTTP/1.1 201 Created" + CRLF, responseContent.get(line++));
+        assertEquals("Location: " + BASE_URI + "/ESTwoPrim(3)" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else if ("3".equals(contentId)) {
+        assertEquals("HTTP/1.1 200 OK" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else if ("4".equals(contentId)) {
+        assertEquals("HTTP/1.1 200 OK" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else if ("5".equals(contentId)) {
+        assertEquals("HTTP/1.1 201 Created" + CRLF, responseContent.get(line++));
+        assertEquals("Location: " + BASE_URI + "/ESTwoPrim(2)" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else if ("6".equals(contentId)) {
+        assertEquals("HTTP/1.1 200 OK" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else {
+        fail();
+      }
+
+      assertEquals(CRLF, responseContent.get(line++));
+    }
+
+    // Close body part (change set)
+    assertEquals(CRLF, responseContent.get(line++));
+    assertTrue(responseContent.get(line++).contains("--changeset_"));
+
+    // Close batch
+    assertEquals(CRLF, responseContent.get(line++));
+    assertTrue(responseContent.get(line++).contains("--batch_"));
+    assertEquals(63, line);
+  }
+
+  @Test
+  public void testMultipleChangeSets() throws BatchException, IOException {
+    final String content = ""
+        + "--batch_12345" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_12345" + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 2" + CRLF
+        + CRLF
+        + "POST /$1/NavPropertyETTwoPrimMany HTTP/1.1" + CRLF
+        + "Host: http://localhost:8080/odata" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "PUT ESAllPrim(1) HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345--" + CRLF
+
+        + "--batch_12345" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_54321" + CRLF
+        + CRLF
+        + "--changeset_54321" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 2" + CRLF
+        + CRLF
+        + "POST /$1/NavPropertyETTwoPrimMany HTTP/1.1" + CRLF
+        + "Host: http://localhost:8080/odata" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_54321" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "PUT ESAllPrim(2) HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_54321--" + CRLF
+
+        + CRLF
+        + "--batch_12345--";
+    final Map<String, List<String>> header = getMimeHeader();
+    final ODataResponse response = new ODataResponse();
+    final BatchHandler batchHandler = buildBatchHandler(content, header);
+
+    batchHandler.process(response);
+
+    BufferedReaderIncludingLineEndings reader =
+        new BufferedReaderIncludingLineEndings(new InputStreamReader(response.getContent()));
+
+    final List<String> responseContent = reader.toList();
+    reader.close();
+
+    int line = 0;
+    assertEquals(49, responseContent.size());
+
+    // Check first change set
+    assertTrue(responseContent.get(line++).contains("--batch_"));
+    assertTrue(responseContent.get(line++).contains("Content-Type: multipart/mixed; boundary=changeset_"));
+
+    for (int i = 0; i < 2; i++) {
+      String contentId = checkChangeSetPartHeader(responseContent, line);
+      line += 6;
+
+      if ("1".equals(contentId)) {
+        assertEquals("HTTP/1.1 200 OK" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else if ("2".equals(contentId)) {
+        assertEquals("HTTP/1.1 201 Created" + CRLF, responseContent.get(line++));
+        assertEquals("Location: " + BASE_URI + "/ESTwoPrim(1)" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else {
+        fail();
+      }
+
+      assertEquals(CRLF, responseContent.get(line++));
+    }
+    // Close body part (1st change set)
+    assertEquals(CRLF, responseContent.get(line++));
+    assertTrue(responseContent.get(line++).contains("--changeset_"));
+
+    // Check second change set
+    assertEquals(CRLF, responseContent.get(line++));
+    assertTrue(responseContent.get(line++).contains("--batch_"));
+    assertTrue(responseContent.get(line++).contains("Content-Type: multipart/mixed; boundary=changeset_"));
+
+    for (int i = 0; i < 2; i++) {
+      String contentId = checkChangeSetPartHeader(responseContent, line);
+      line += 6;
+
+      if ("1".equals(contentId)) {
+        assertEquals("HTTP/1.1 200 OK" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else if ("2".equals(contentId)) {
+        assertEquals("HTTP/1.1 201 Created" + CRLF, responseContent.get(line++));
+        assertEquals("Location: " + BASE_URI + "/ESTwoPrim(2)" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else {
+        fail();
+      }
+
+      assertEquals(CRLF, responseContent.get(line++));
+    }
+    // Close body part (2nd change set)
+    assertEquals(CRLF, responseContent.get(line++));
+    assertTrue(responseContent.get(line++).contains("--changeset_"));
+
+    // Close batch
+    assertEquals(CRLF, responseContent.get(line++));
+    assertTrue(responseContent.get(line++).contains("--batch_"));
+
+    assertEquals(49, line);
+  }
+
+  @Test
+  public void testMineBodyPartTransitiv() throws BatchException, IOException {
+    final String content = ""
+        + "--batch_12345" + CRLF
+        + "Content-Type: multipart/mixed; boundary=changeset_12345" + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 4" + CRLF
+        + CRLF
+        + "POST $3/NavPropertyETTwoPrimOne HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 2" + CRLF
+        + CRLF
+        + "POST /$1/NavPropertyETTwoPrimMany HTTP/1.1" + CRLF
+        + "Host: http://localhost:8080/odata" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 1" + CRLF
+        + CRLF
+        + "PUT ESAllPrim(1) HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345" + CRLF
+        + "Content-Type: application/http" + CRLF
+        + "Content-Transfer-Encoding: binary" + CRLF
+        + "Content-Id: 3" + CRLF
+        + CRLF
+        + "POST $2/NavPropertyETAllPrimMany HTTP/1.1" + CRLF
+        + "Content-Type: application/json;odata=verbose" + CRLF
+        + CRLF
+        + CRLF
+        + "--changeset_12345--" + CRLF
+
+        + CRLF
+        + "--batch_12345--";
+
+    final Map<String, List<String>> header = getMimeHeader();
+    final ODataResponse response = new ODataResponse();
+    final BatchHandler batchHandler = buildBatchHandler(content, header);
+
+    batchHandler.process(response);
+
+    BufferedReaderIncludingLineEndings reader =
+        new BufferedReaderIncludingLineEndings(new InputStreamReader(response.getContent()));
+
+    final List<String> responseContent = reader.toList();
+    reader.close();
+
+    int line = 0;
+    assertEquals(45, responseContent.size());
+
+    // Check change set
+    assertTrue(responseContent.get(line++).contains("--batch_"));
+    assertTrue(responseContent.get(line++).contains("Content-Type: multipart/mixed; boundary=changeset_"));
+
+    for (int i = 0; i < 4; i++) {
+      String contentId = checkChangeSetPartHeader(responseContent, line);
+      line += 6;
+
+      if ("1".equals(contentId)) {
+        assertEquals("HTTP/1.1 200 OK" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else if ("2".equals(contentId)) {
+        assertEquals("HTTP/1.1 201 Created" + CRLF, responseContent.get(line++));
+        assertEquals("Location: " + BASE_URI + "/ESTwoPrim(1)" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else if ("3".equals(contentId)) {
+        assertEquals("HTTP/1.1 201 Created" + CRLF, responseContent.get(line++));
+        assertEquals("Location: " + BASE_URI + "/ESAllPrim(2)" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else if ("4".equals(contentId)) {
+        assertEquals("HTTP/1.1 201 Created" + CRLF, responseContent.get(line++));
+        assertEquals("Location: " + BASE_URI + "/ESTwoPrim(3)" + CRLF, responseContent.get(line++));
+        assertEquals("Content-Length: 0" + CRLF, responseContent.get(line++));
+      } else {
+        fail();
+      }
+
+      assertEquals(CRLF, responseContent.get(line++));
+    }
+
+    // Close body part (change set)
+    assertEquals(CRLF, responseContent.get(line++));
+    assertTrue(responseContent.get(line++).contains("--changeset_"));
+
+    // Close batch
+    assertEquals(CRLF, responseContent.get(line++));
+    assertTrue(responseContent.get(line++).contains("--batch_"));
+    assertEquals(45, line);
+  }
+
+  @Before
+  public void setup() {
+    handler = null;
+    entityCounter = 1;
+  }
+
+  private String checkChangeSetPartHeader(final List<String> response, int line) {
+    assertEquals(CRLF, response.get(line++));
+    assertTrue(response.get(line++).contains("--changeset_"));
+    assertEquals("Content-Type: application/http" + CRLF, response.get(line++));
+    assertEquals("Content-Transfer-Encoding: binary" + CRLF, response.get(line++));
+
+    assertTrue(response.get(line).contains("Content-Id:"));
+    String contentId = response.get(line).split(":")[1].trim();
+    line++;
+
+    assertEquals(CRLF, response.get(line++));
+
+    return contentId;
+  }
+
+  /*
+   * Helper methods
+   */
+
+  private Map<String, List<String>> getMimeHeader() {
+    final Map<String, List<String>> header = new HashMap<String, List<String>>();
+    header.put(HttpHeader.CONTENT_TYPE, Arrays.asList(new String[] { BATCH_CONTENT_TYPE }));
+
+    return header;
+  }
+
+  private BatchHandler buildBatchHandler(final String content, Map<String, List<String>> header) throws BatchException,
+      UnsupportedEncodingException {
+
+    final ODataRequest request = buildODataRequest(content, header);
+    final ODataHandler oDataHandler = buildODataHandler(request);
+    final BatchProcessor batchProcessor = new BatchProcessorImpl();
+
+    return new BatchHandler(oDataHandler, request, batchProcessor, true);
+  }
+
+  private ODataHandler buildODataHandler(ODataRequest request) {
+    handler = mock(ODataHandler.class);
+    when(handler.process(request)).thenCallRealMethod();
+
+    return handler;
+  }
+
+  private ODataRequest buildODataRequest(String content, Map<String, List<String>> header)
+      throws UnsupportedEncodingException {
+    final ODataRequest request = new ODataRequest();
+
+    for (final String key : header.keySet()) {
+      request.addHeader(key, header.get(key));
+    }
+
+    request.setMethod(HttpMethod.POST);
+    request.setRawBaseUri(BASE_URI);
+    request.setRawODataPath(BATCH_ODATA_PATH);
+    request.setRawQueryPath("");
+    request.setRawRequestUri(BATCH_REQUEST_URI);
+    request.setRawServiceResolutionUri("");
+
+    request.setBody(new ByteArrayInputStream(content.getBytes("UTF-8")));
+
+    return request;
+  }
+
+  /**
+   * Batch processor
+   */
+  private class BatchProcessorImpl implements BatchProcessor {
+    @Override
+    public void init(OData odata, ServiceMetadata serviceMetadata) {}
+
+    @Override
+    public List<ODataResponse> executeChangeSet(BatchOperation operation, List<ODataRequest> requests,
+        BatchRequestPart requestPart) {
+      List<ODataResponse> responses = new ArrayList<ODataResponse>();
+
+      for (ODataRequest request : requests) {
+        // Mock the processor of the changeset requests
+        when(handler.process(request)).then(new Answer<ODataResponse>() {
+          @Override
+          public ODataResponse answer(InvocationOnMock invocation) throws Throwable {
+            Object[] arguments = invocation.getArguments();
+
+            return buildResponse((ODataRequest) arguments[0]);
+          }
+        });
+
+        try {
+          responses.add(operation.handleODataRequest(request, requestPart));
+        } catch (BatchException e) {
+          fail();
+        }
+      }
+
+      return responses;
+    }
+
+    @Override
+    public void executeBatch(BatchOperation operation, ODataRequest request, ODataResponse response) {
+      try {
+        final List<BatchRequestPart> parts = operation.parseBatchRequest(request.getBody());
+        final List<ODataResponsePart> responseParts = new ArrayList<ODataResponsePart>();
+
+        for (BatchRequestPart part : parts) {
+          responseParts.add(operation.handleBatchRequest(part));
+        }
+
+        operation.writeResponseParts(responseParts, response);
+      } catch (BatchException e) {
+        throw new ODataRuntimeException(e);
+      } catch (IOException e) {
+        throw new ODataRuntimeException(e);
+      }
+    }
+  }
+
+  private ODataResponse buildResponse(ODataRequest request) {
+    final ODataResponse oDataResponse = new ODataResponse();
+
+    if (request.getMethod() == HttpMethod.POST) {
+      oDataResponse.setStatusCode(HttpStatusCode.CREATED.getStatusCode());
+      oDataResponse.setHeader(HttpHeader.LOCATION, createResourceUri(request));
+    } else {
+      oDataResponse.setStatusCode(HttpStatusCode.OK.getStatusCode());
+    }
+
+    final String contentId = request.getHeader(BatchParserCommon.HTTP_CONTENT_ID);
+    if (contentId != null) {
+      oDataResponse.setHeader(BatchParserCommon.HTTP_CONTENT_ID, contentId);
+    }
+
+    return oDataResponse;
+  }
+
+  private String createResourceUri(final ODataRequest request) {
+    final String parts[] = request.getRawODataPath().split("/");
+    String oDataPath = "";
+
+    if (parts.length == 2) {
+      // Entity Collection
+      oDataPath = parts[1];
+    } else {
+      // Navigationproperty
+
+      final String navProperty = parts[parts.length - 1];
+      if (navProperty.equals("NavPropertyETTwoPrimMany")) {
+        oDataPath = "ESTwoPrim";
+      } else if (navProperty.equals("NavPropertyETAllPrimMany")) {
+        oDataPath = "ESAllPrim";
+      } else if (navProperty.equals("NavPropertyETTwoPrimOne")) {
+        oDataPath = "ESTwoPrim";
+      }
+    }
+
+    return BASE_URI + "/" + oDataPath + "(" + entityCounter++ + ")";
+  }
+}