You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@olingo.apache.org by fm...@apache.org on 2013/07/26 13:22:28 UTC

[23/51] [partial] initial commit

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/ff2b0a0e/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchRequestParser.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchRequestParser.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchRequestParser.java
new file mode 100644
index 0000000..b24bbc1
--- /dev/null
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchRequestParser.java
@@ -0,0 +1,563 @@
+/*******************************************************************************
+ * 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.odata2.core.batch;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.regex.MatchResult;
+import java.util.regex.Pattern;
+
+import org.apache.olingo.odata2.api.batch.BatchException;
+import org.apache.olingo.odata2.api.batch.BatchPart;
+import org.apache.olingo.odata2.api.commons.HttpHeaders;
+import org.apache.olingo.odata2.api.commons.ODataHttpMethod;
+import org.apache.olingo.odata2.api.ep.EntityProviderBatchProperties;
+import org.apache.olingo.odata2.api.processor.ODataRequest;
+import org.apache.olingo.odata2.api.uri.PathInfo;
+import org.apache.olingo.odata2.api.uri.PathSegment;
+import org.apache.olingo.odata2.core.ODataPathSegmentImpl;
+import org.apache.olingo.odata2.core.PathInfoImpl;
+import org.apache.olingo.odata2.core.commons.Decoder;
+import org.apache.olingo.odata2.core.exception.ODataRuntimeException;
+
+/**
+ * @author SAP AG
+ */
+public class BatchRequestParser {
+  private static final String LF = "\n";
+  private static final String REG_EX_OPTIONAL_WHITESPACE = "\\s?";
+  private static final String REG_EX_ZERO_OR_MORE_WHITESPACES = "\\s*";
+  private static final String ANY_CHARACTERS = ".*";
+
+  private static final Pattern REG_EX_BLANK_LINE = Pattern.compile("(|" + REG_EX_ZERO_OR_MORE_WHITESPACES + ")");
+  private static final Pattern REG_EX_HEADER = Pattern.compile("([a-zA-Z\\-]+):" + REG_EX_OPTIONAL_WHITESPACE + "(.*)" + REG_EX_ZERO_OR_MORE_WHITESPACES);
+  private static final Pattern REG_EX_VERSION = Pattern.compile("(?:HTTP/[0-9]\\.[0-9])");
+  private static final Pattern REG_EX_ANY_BOUNDARY_STRING = Pattern.compile("--" + ANY_CHARACTERS + REG_EX_ZERO_OR_MORE_WHITESPACES);
+  private static final Pattern REG_EX_REQUEST_LINE = Pattern.compile("(GET|POST|PUT|DELETE|MERGE|PATCH)\\s(.*)\\s?" + REG_EX_VERSION + REG_EX_ZERO_OR_MORE_WHITESPACES);
+  private static final Pattern REG_EX_BOUNDARY_PARAMETER = Pattern.compile(REG_EX_OPTIONAL_WHITESPACE + "boundary=(\".*\"|.*)" + REG_EX_ZERO_OR_MORE_WHITESPACES);
+  private static final Pattern REG_EX_CONTENT_TYPE = Pattern.compile(REG_EX_OPTIONAL_WHITESPACE + BatchConstants.MULTIPART_MIXED);
+  private static final Pattern REG_EX_QUERY_PARAMETER = Pattern.compile("((?:\\$|)[^=]+)=([^=]+)");
+
+  private static final String REG_EX_BOUNDARY = "([a-zA-Z0-9_\\-\\.'\\+]{1,70})|\"([a-zA-Z0-9_\\-\\.'\\+\\s\\(\\),/:=\\?]{1,69}[a-zA-Z0-9_\\-\\.'\\+\\(\\),/:=\\?])\""; // See RFC 2046
+  private String baseUri;
+  private PathInfo batchRequestPathInfo;
+  private String contentTypeMime;
+  private String boundary;
+  private String currentMimeHeaderContentId;
+  private int currentLineNumber = 0;
+  private static Set<String> HTTP_CHANGESET_METHODS;
+  private static Set<String> HTTP_BATCH_METHODS;
+
+  static {
+    HTTP_CHANGESET_METHODS = new HashSet<String>();
+    HTTP_CHANGESET_METHODS.add("POST");
+    HTTP_CHANGESET_METHODS.add("PUT");
+    HTTP_CHANGESET_METHODS.add("DELETE");
+    HTTP_CHANGESET_METHODS.add("MERGE");
+    HTTP_CHANGESET_METHODS.add("PATCH");
+
+    HTTP_BATCH_METHODS = new HashSet<String>();
+    HTTP_BATCH_METHODS.add("GET");
+  }
+
+  public BatchRequestParser(final String contentType, final EntityProviderBatchProperties properties) {
+    contentTypeMime = contentType;
+    batchRequestPathInfo = properties.getPathInfo();
+  }
+
+  public List<BatchPart> parse(final InputStream in) throws BatchException {
+    Scanner scanner = new Scanner(in, "UTF-8").useDelimiter(LF);
+    baseUri = getBaseUri();
+    List<BatchPart> requestList;
+    try {
+      requestList = parseBatchRequest(scanner);
+    } finally {// NOPMD (suppress DoNotThrowExceptionInFinally)
+      scanner.close();
+      try {
+        in.close();
+      } catch (IOException e) {
+        throw new ODataRuntimeException(e);
+      }
+    }
+    return requestList;
+  }
+
+  private List<BatchPart> parseBatchRequest(final Scanner scanner) throws BatchException {
+    List<BatchPart> requests = new LinkedList<BatchPart>();
+    if (contentTypeMime != null) {
+      boundary = getBoundary(contentTypeMime);
+      parsePreamble(scanner);
+      String closeDelimiter = "--" + boundary + "--" + REG_EX_ZERO_OR_MORE_WHITESPACES;
+      while (scanner.hasNext() && !scanner.hasNext(closeDelimiter)) {
+        requests.add(parseMultipart(scanner, boundary, false));
+        parseNewLine(scanner);
+      }
+      if (scanner.hasNext(closeDelimiter)) {
+        scanner.next(closeDelimiter);
+        currentLineNumber++;
+      } else {
+        throw new BatchException(BatchException.MISSING_CLOSE_DELIMITER.addContent(currentLineNumber));
+      }
+    } else {
+      throw new BatchException(BatchException.MISSING_CONTENT_TYPE);
+    }
+    return requests;
+
+  }
+
+  //The method parses additional information prior to the first boundary delimiter line
+  private void parsePreamble(final Scanner scanner) {
+    while (scanner.hasNext() && !scanner.hasNext(REG_EX_ANY_BOUNDARY_STRING)) {
+      scanner.next();
+      currentLineNumber++;
+    }
+  }
+
+  private BatchPart parseMultipart(final Scanner scanner, final String boundary, final boolean isChangeSet) throws BatchException {
+    Map<String, String> mimeHeaders = new HashMap<String, String>();
+    BatchPart multipart = null;
+    List<ODataRequest> requests = new ArrayList<ODataRequest>();
+    if (scanner.hasNext("--" + boundary + REG_EX_ZERO_OR_MORE_WHITESPACES)) {
+      scanner.next();
+      currentLineNumber++;
+      mimeHeaders = parseHeaders(scanner);
+      currentMimeHeaderContentId = mimeHeaders.get(BatchConstants.HTTP_CONTENT_ID.toLowerCase(Locale.ENGLISH));
+
+      String contentType = mimeHeaders.get(HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.ENGLISH));
+      if (contentType == null) {
+        throw new BatchException(BatchException.MISSING_CONTENT_TYPE);
+      }
+      if (isChangeSet) {
+        if (BatchConstants.HTTP_APPLICATION_HTTP.equalsIgnoreCase(contentType)) {
+          validateEncoding(mimeHeaders.get(BatchConstants.HTTP_CONTENT_TRANSFER_ENCODING.toLowerCase(Locale.ENGLISH)));
+          parseNewLine(scanner);// mandatory
+
+          requests.add(parseRequest(scanner, isChangeSet));
+          multipart = new BatchPartImpl(false, requests);
+        } else {
+          throw new BatchException(BatchException.INVALID_CONTENT_TYPE.addContent(BatchConstants.HTTP_APPLICATION_HTTP));
+        }
+      } else {
+        if (BatchConstants.HTTP_APPLICATION_HTTP.equalsIgnoreCase(contentType)) {
+          validateEncoding(mimeHeaders.get(BatchConstants.HTTP_CONTENT_TRANSFER_ENCODING.toLowerCase(Locale.ENGLISH)));
+          parseNewLine(scanner);// mandatory
+          requests.add(parseRequest(scanner, isChangeSet));
+          multipart = new BatchPartImpl(false, requests);
+        } else if (contentType.matches(REG_EX_OPTIONAL_WHITESPACE + BatchConstants.MULTIPART_MIXED + ANY_CHARACTERS)) {
+          String changeSetBoundary = getBoundary(contentType);
+          if (boundary.equals(changeSetBoundary)) {
+            throw new BatchException(BatchException.INVALID_CHANGESET_BOUNDARY.addContent(currentLineNumber));
+          }
+          List<ODataRequest> changeSetRequests = new LinkedList<ODataRequest>();
+          parseNewLine(scanner);// mandatory
+          Pattern changeSetCloseDelimiter = Pattern.compile("--" + changeSetBoundary + "--" + REG_EX_ZERO_OR_MORE_WHITESPACES);
+          while (!scanner.hasNext(changeSetCloseDelimiter)) {
+            BatchPart part = parseMultipart(scanner, changeSetBoundary, true);
+            if (part.getRequests().size() == 1) {
+              changeSetRequests.add(part.getRequests().get(0));
+            }
+          }
+          scanner.next(changeSetCloseDelimiter);
+          currentLineNumber++;
+          multipart = new BatchPartImpl(true, changeSetRequests);
+        } else {
+          throw new BatchException(BatchException.INVALID_CONTENT_TYPE.addContent(BatchConstants.MULTIPART_MIXED + " or " + BatchConstants.HTTP_APPLICATION_HTTP));
+        }
+      }
+    } else if (scanner.hasNext(boundary + REG_EX_ZERO_OR_MORE_WHITESPACES)) {
+      currentLineNumber++;
+      throw new BatchException(BatchException.INVALID_BOUNDARY_DELIMITER.addContent(currentLineNumber));
+    } else if (scanner.hasNext(REG_EX_ANY_BOUNDARY_STRING)) {
+      currentLineNumber++;
+      throw new BatchException(BatchException.NO_MATCH_WITH_BOUNDARY_STRING.addContent(boundary).addContent(currentLineNumber));
+    } else {
+      currentLineNumber++;
+      throw new BatchException(BatchException.MISSING_BOUNDARY_DELIMITER.addContent(currentLineNumber));
+    }
+    return multipart;
+
+  }
+
+  private ODataRequest parseRequest(final Scanner scanner, final boolean isChangeSet) throws BatchException {
+    try {
+    ODataRequest request;
+    if (scanner.hasNext(REG_EX_REQUEST_LINE)) {
+      scanner.next(REG_EX_REQUEST_LINE);
+      currentLineNumber++;
+      String method = null;
+      String uri = null;
+      MatchResult result = scanner.match();
+      if (result.groupCount() == 2) {
+        method = result.group(1);
+        uri = result.group(2).trim();
+      } else {
+        currentLineNumber++;
+        throw new BatchException(BatchException.INVALID_REQUEST_LINE.addContent(scanner.next()).addContent(currentLineNumber));
+      }
+      PathInfo pathInfo = parseRequestUri(uri);
+      Map<String, String> queryParameters = parseQueryParameters(uri);
+      if (isChangeSet) {
+        if (!HTTP_CHANGESET_METHODS.contains(method)) {
+          throw new BatchException(BatchException.INVALID_CHANGESET_METHOD.addContent(currentLineNumber));
+        }
+      } else if (!HTTP_BATCH_METHODS.contains(method)) {
+        throw new BatchException(BatchException.INVALID_QUERY_OPERATION_METHOD.addContent(currentLineNumber));
+      }
+      ODataHttpMethod httpMethod = ODataHttpMethod.valueOf(method);
+      Map<String, List<String>> headers = parseRequestHeaders(scanner);
+      if (currentMimeHeaderContentId != null) {
+        List<String> headerList = new ArrayList<String>();
+        headerList.add(currentMimeHeaderContentId);
+        headers.put(BatchConstants.MIME_HEADER_CONTENT_ID.toLowerCase(Locale.ENGLISH), headerList);
+      }
+
+      String contentType = getContentTypeHeader(headers);
+      List<String> acceptHeaders = getAcceptHeader(headers);
+      List<Locale> acceptLanguages = getAcceptLanguageHeader(headers);
+      parseNewLine(scanner);
+      InputStream body;
+        body = new ByteArrayInputStream("".getBytes("UTF-8"));
+      if (isChangeSet) {
+        body = parseBody(scanner);
+      }
+      if (contentType != null) {
+        request = ODataRequest.method(httpMethod)
+            .queryParameters(queryParameters)
+            .requestHeaders(headers)
+            .pathInfo(pathInfo)
+            .acceptableLanguages(acceptLanguages)
+            .contentType(contentType)
+            .body(body)
+            .acceptHeaders(acceptHeaders)
+            .build();
+      } else {
+        request = ODataRequest.method(httpMethod)
+            .queryParameters(queryParameters)
+            .requestHeaders(headers)
+            .pathInfo(pathInfo)
+            .acceptableLanguages(acceptLanguages)
+            .body(body)
+            .acceptHeaders(acceptHeaders)
+            .build();
+      }
+    } else {
+      currentLineNumber++;
+      throw new BatchException(BatchException.INVALID_REQUEST_LINE.addContent(scanner.next()).addContent(currentLineNumber));
+    }
+    return request;
+    } catch (UnsupportedEncodingException e) {
+      throw new ODataRuntimeException(e);
+    }
+  }
+
+  private Map<String, List<String>> parseRequestHeaders(final Scanner scanner) throws BatchException {
+    Map<String, List<String>> headers = new HashMap<String, List<String>>();
+    while (scanner.hasNext() && !scanner.hasNext(REG_EX_BLANK_LINE)) {
+      if (scanner.hasNext(REG_EX_HEADER)) {
+        scanner.next(REG_EX_HEADER);
+        currentLineNumber++;
+        MatchResult result = scanner.match();
+        if (result.groupCount() == 2) {
+          String headerName = result.group(1).trim().toLowerCase(Locale.ENGLISH);
+          String headerValue = result.group(2).trim();
+          if (HttpHeaders.ACCEPT.equalsIgnoreCase(headerName)) {
+            List<String> acceptHeaders = parseAcceptHeaders(headerValue);
+            headers.put(headerName, acceptHeaders);
+          } else if (HttpHeaders.ACCEPT_LANGUAGE.equalsIgnoreCase(headerName)) {
+            List<String> acceptLanguageHeaders = parseAcceptableLanguages(headerValue);
+            headers.put(headerName, acceptLanguageHeaders);
+          }
+          else if (!BatchConstants.HTTP_CONTENT_ID.equalsIgnoreCase(headerName)) {
+            if (headers.containsKey(headerName)) {
+              headers.get(headerName).add(headerValue);
+            } else {
+              List<String> headerList = new ArrayList<String>();
+              headerList.add(headerValue);
+              headers.put(headerName, headerList);
+            }
+          } else {
+            List<String> headerList = new ArrayList<String>();
+            headerList.add(headerValue);
+            headers.put(BatchConstants.REQUEST_HEADER_CONTENT_ID.toLowerCase(Locale.ENGLISH), headerList);
+          }
+        }
+      } else {
+        currentLineNumber++;
+        throw new BatchException(BatchException.INVALID_HEADER.addContent(scanner.next()).addContent(currentLineNumber));
+      }
+    }
+    return headers;
+  }
+
+  private PathInfo parseRequestUri(final String uri) throws BatchException {
+    PathInfoImpl pathInfo = new PathInfoImpl();
+    pathInfo.setServiceRoot(batchRequestPathInfo.getServiceRoot());
+    pathInfo.setPrecedingPathSegment(batchRequestPathInfo.getPrecedingSegments());
+    Scanner uriScanner = new Scanner(uri);
+    Pattern regexRequestUri = Pattern.compile("(?:" + baseUri + ")?/?([^?]+)(\\?.*)?");
+    if (uriScanner.hasNext(regexRequestUri)) {
+      uriScanner.next(regexRequestUri);
+      MatchResult result = uriScanner.match();
+      if (result.groupCount() == 2) {
+        String odataPathSegmentsAsString = result.group(1);
+        String queryParametersAsString = result.group(2) != null ? result.group(2) : "";
+        pathInfo.setODataPathSegment(parseODataPathSegments(odataPathSegmentsAsString));
+        try {
+          if (!odataPathSegmentsAsString.startsWith("$")) {
+            String requestUri = baseUri + "/" + odataPathSegmentsAsString + queryParametersAsString;
+            pathInfo.setRequestUri(new URI(requestUri));
+          }
+        } catch (URISyntaxException e) {
+          uriScanner.close();
+          throw new BatchException(BatchException.INVALID_URI, e);
+        }
+      } else {
+        uriScanner.close();
+        throw new BatchException(BatchException.INVALID_URI);
+      }
+    } else {
+      uriScanner.close();
+      throw new BatchException(BatchException.INVALID_URI);
+    }
+    uriScanner.close();
+    return pathInfo;
+  }
+
+  private Map<String, String> parseQueryParameters(final String uri) throws BatchException {
+    Scanner uriScanner = new Scanner(uri);
+    Map<String, String> queryParametersMap = new HashMap<String, String>();
+    Pattern regex = Pattern.compile("(?:" + baseUri + ")?/?" + "[^?]+" + "\\?(.*)");
+    if (uriScanner.hasNext(regex)) {
+      uriScanner.next(regex);
+      MatchResult uriResult = uriScanner.match();
+      if (uriResult.groupCount() == 1) {
+        String queryParams = uriResult.group(1);
+        Scanner queryParamsScanner = new Scanner(queryParams).useDelimiter("&");
+        while (queryParamsScanner.hasNext(REG_EX_QUERY_PARAMETER)) {
+          queryParamsScanner.next(REG_EX_QUERY_PARAMETER);
+          MatchResult result = queryParamsScanner.match();
+          if (result.groupCount() == 2) {
+            String systemQueryOption = result.group(1);
+            String value = result.group(2);
+            queryParametersMap.put(systemQueryOption, Decoder.decode(value));
+          } else {
+            queryParamsScanner.close();
+            throw new BatchException(BatchException.INVALID_QUERY_PARAMETER);
+          }
+        }
+        queryParamsScanner.close();
+
+      } else {
+        uriScanner.close();
+        throw new BatchException(BatchException.INVALID_URI);
+      }
+    }
+    uriScanner.close();
+    return queryParametersMap;
+  }
+
+  private List<PathSegment> parseODataPathSegments(final String odataPathSegmentsAsString) {
+    Scanner pathSegmentScanner = new Scanner(odataPathSegmentsAsString).useDelimiter("/");
+    List<PathSegment> odataPathSegments = new ArrayList<PathSegment>();
+    while (pathSegmentScanner.hasNext()) {
+      odataPathSegments.add(new ODataPathSegmentImpl(pathSegmentScanner.next(), null));
+    }
+    pathSegmentScanner.close();
+    return odataPathSegments;
+  }
+
+  private List<String> parseAcceptHeaders(final String headerValue) throws BatchException {
+    return AcceptParser.parseAcceptHeaders(headerValue);
+  }
+
+  private List<String> parseAcceptableLanguages(final String headerValue) throws BatchException {
+    return AcceptParser.parseAcceptableLanguages(headerValue);
+  }
+
+  private InputStream parseBody(final Scanner scanner) {
+    try {
+    String body = null;
+    InputStream requestBody;
+    while (scanner.hasNext() && !scanner.hasNext(REG_EX_ANY_BOUNDARY_STRING)) {
+      if (!scanner.hasNext(REG_EX_ZERO_OR_MORE_WHITESPACES)) {
+        if (body == null) {
+          body = scanner.next();
+        } else {
+          body = body + LF + scanner.next();
+        }
+      } else {
+        scanner.next();
+      }
+      currentLineNumber++;
+    }
+    if (body != null) {
+        requestBody = new ByteArrayInputStream(body.getBytes("UTF-8"));
+    } else {
+      requestBody = new ByteArrayInputStream("".getBytes("UTF-8"));
+    }
+    return requestBody;
+    } catch (UnsupportedEncodingException e) {
+      throw new ODataRuntimeException(e);
+    }
+  }
+
+  private String getBoundary(final String contentType) throws BatchException {
+    Scanner contentTypeScanner = new Scanner(contentType).useDelimiter(";\\s?");
+    if (contentTypeScanner.hasNext(REG_EX_CONTENT_TYPE)) {
+      contentTypeScanner.next(REG_EX_CONTENT_TYPE);
+    } else {
+      contentTypeScanner.close();
+      throw new BatchException(BatchException.INVALID_CONTENT_TYPE.addContent(BatchConstants.MULTIPART_MIXED));
+    }
+    if (contentTypeScanner.hasNext(REG_EX_BOUNDARY_PARAMETER)) {
+      contentTypeScanner.next(REG_EX_BOUNDARY_PARAMETER);
+      MatchResult result = contentTypeScanner.match();
+      contentTypeScanner.close();
+      if (result.groupCount() == 1 && result.group(1).trim().matches(REG_EX_BOUNDARY)) {
+        return trimQuota(result.group(1).trim());
+      } else {
+        throw new BatchException(BatchException.INVALID_BOUNDARY);
+      }
+    } else {
+      contentTypeScanner.close();
+      throw new BatchException(BatchException.MISSING_PARAMETER_IN_CONTENT_TYPE);
+    }
+  }
+
+  private void validateEncoding(final String encoding) throws BatchException {
+    if (!BatchConstants.BINARY_ENCODING.equalsIgnoreCase(encoding)) {
+      throw new BatchException(BatchException.INVALID_CONTENT_TRANSFER_ENCODING);
+    }
+  }
+
+  private Map<String, String> parseHeaders(final Scanner scanner) throws BatchException {
+    Map<String, String> headers = new HashMap<String, String>();
+    while (scanner.hasNext() && !(scanner.hasNext(REG_EX_BLANK_LINE))) {
+      if (scanner.hasNext(REG_EX_HEADER)) {
+        scanner.next(REG_EX_HEADER);
+        currentLineNumber++;
+        MatchResult result = scanner.match();
+        if (result.groupCount() == 2) {
+          String headerName = result.group(1).trim().toLowerCase(Locale.ENGLISH);
+          String headerValue = result.group(2).trim();
+          headers.put(headerName, headerValue);
+        }
+      } else {
+        throw new BatchException(BatchException.INVALID_HEADER.addContent(scanner.next()));
+      }
+    }
+    return headers;
+  }
+
+  private void parseNewLine(final Scanner scanner) throws BatchException {
+    if (scanner.hasNext() && scanner.hasNext(REG_EX_BLANK_LINE)) {
+      scanner.next();
+      currentLineNumber++;
+    } else {
+      currentLineNumber++;
+      if (scanner.hasNext()) {
+        throw new BatchException(BatchException.MISSING_BLANK_LINE.addContent(scanner.next()).addContent(currentLineNumber));
+      } else {
+        throw new BatchException(BatchException.TRUNCATED_BODY.addContent(currentLineNumber));
+
+      }
+    }
+  }
+
+  private String getBaseUri() throws BatchException {
+    if (batchRequestPathInfo != null) {
+      if (batchRequestPathInfo.getServiceRoot() != null) {
+        String baseUri = batchRequestPathInfo.getServiceRoot().toASCIIString();
+        if (baseUri.lastIndexOf('/') == baseUri.length() - 1) {
+          baseUri = baseUri.substring(0, baseUri.length() - 1);
+        }
+        for (PathSegment precedingPS : batchRequestPathInfo.getPrecedingSegments()) {
+          baseUri = baseUri + "/" + precedingPS.getPath();
+        }
+        return baseUri;
+      }
+    } else {
+      throw new BatchException(BatchException.INVALID_PATHINFO);
+    }
+    return null;
+  }
+
+  private String trimQuota(String boundary) {
+    if (boundary.matches("\".*\"")) {
+      boundary = boundary.replace("\"", "");
+    }
+    boundary = boundary.replaceAll("\\)", "\\\\)");
+    boundary = boundary.replaceAll("\\(", "\\\\(");
+    boundary = boundary.replaceAll("\\?", "\\\\?");
+    boundary = boundary.replaceAll("\\+", "\\\\+");
+    return boundary;
+  }
+
+  private List<String> getAcceptHeader(final Map<String, List<String>> headers) {
+    List<String> acceptHeaders = new ArrayList<String>();
+    List<String> requestAcceptHeaderList = headers.get(HttpHeaders.ACCEPT.toLowerCase(Locale.ENGLISH));
+
+    if (requestAcceptHeaderList != null) {
+      acceptHeaders = requestAcceptHeaderList;
+    }
+    return acceptHeaders;
+  }
+
+  private List<Locale> getAcceptLanguageHeader(final Map<String, List<String>> headers) {
+    List<String> requestAcceptLanguageList = headers.get(HttpHeaders.ACCEPT_LANGUAGE.toLowerCase(Locale.ENGLISH));
+    List<Locale> acceptLanguages = new ArrayList<Locale>();
+    if (requestAcceptLanguageList != null) {
+      for (String acceptLanguage : requestAcceptLanguageList) {
+        String[] part = acceptLanguage.split("-");
+        String language = part[0];
+        String country = "";
+        if (part.length == 2) {
+          country = part[part.length - 1];
+        }
+        Locale locale = new Locale(language, country);
+        acceptLanguages.add(locale);
+      }
+    }
+    return acceptLanguages;
+  }
+
+  private String getContentTypeHeader(final Map<String, List<String>> headers) {
+    List<String> requestContentTypeList = headers.get(HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.ENGLISH));
+    String contentType = null;
+    if (requestContentTypeList != null) {
+      for (String requestContentType : requestContentTypeList) {
+        contentType = contentType != null ? contentType + "," + requestContentType : requestContentType;
+      }
+    }
+    return contentType;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/ff2b0a0e/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchResponsePartImpl.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchResponsePartImpl.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchResponsePartImpl.java
new file mode 100644
index 0000000..486bbdf
--- /dev/null
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchResponsePartImpl.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * 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.odata2.core.batch;
+
+import java.util.List;
+
+import org.apache.olingo.odata2.api.batch.BatchResponsePart;
+import org.apache.olingo.odata2.api.processor.ODataResponse;
+
+public class BatchResponsePartImpl extends BatchResponsePart {
+  private List<ODataResponse> responses;
+  private boolean isChangeSet;
+
+  @Override
+  public List<ODataResponse> getResponses() {
+    return responses;
+  }
+
+  @Override
+  public boolean isChangeSet() {
+    return isChangeSet;
+  }
+
+  public class BatchResponsePartBuilderImpl extends BatchResponsePartBuilder {
+    private List<ODataResponse> responses;
+    private boolean isChangeSet;
+
+    @Override
+    public BatchResponsePart build() {
+      BatchResponsePartImpl.this.responses = responses;
+      BatchResponsePartImpl.this.isChangeSet = isChangeSet;
+      return BatchResponsePartImpl.this;
+    }
+
+    @Override
+    public BatchResponsePartBuilder responses(final List<ODataResponse> responses) {
+      this.responses = responses;
+      return this;
+    }
+
+    @Override
+    public BatchResponsePartBuilder changeSet(final boolean isChangeSet) {
+      this.isChangeSet = isChangeSet;
+      return this;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/ff2b0a0e/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchResponseWriter.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchResponseWriter.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchResponseWriter.java
new file mode 100644
index 0000000..e705724
--- /dev/null
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/batch/BatchResponseWriter.java
@@ -0,0 +1,136 @@
+/*******************************************************************************
+ * 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.odata2.core.batch;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.olingo.odata2.api.batch.BatchException;
+import org.apache.olingo.odata2.api.batch.BatchResponsePart;
+import org.apache.olingo.odata2.api.commons.HttpHeaders;
+import org.apache.olingo.odata2.api.commons.HttpStatusCodes;
+import org.apache.olingo.odata2.api.exception.ODataMessageException;
+import org.apache.olingo.odata2.api.processor.ODataResponse;
+
+public class BatchResponseWriter {
+  private static final String COLON = ":";
+  private static final String SP = " ";
+  private static final String LF = "\r\n";
+  private StringBuilder writer = new StringBuilder();
+
+  public ODataResponse writeResponse(final List<BatchResponsePart> batchResponseParts) throws BatchException {
+    String boundary = generateBoundary("batch");
+    appendResponseBody(batchResponseParts, boundary);
+    String batchResponseBody = writer.toString();
+    return ODataResponse.entity(batchResponseBody).status(HttpStatusCodes.ACCEPTED).
+        header(HttpHeaders.CONTENT_TYPE, BatchConstants.MULTIPART_MIXED + "; boundary=" + boundary)
+        .header(HttpHeaders.CONTENT_LENGTH, "" + batchResponseBody.length()).build();
+
+  }
+
+  private void appendChangeSet(final BatchResponsePart batchResponsePart) throws BatchException {
+    String boundary = generateBoundary("changeset");
+    writer.append(HttpHeaders.CONTENT_TYPE).append(COLON).append(SP).append("multipart/mixed; boundary=" + boundary).append(LF).append(LF);
+    for (ODataResponse response : batchResponsePart.getResponses()) {
+      writer.append("--").append(boundary).append(LF);
+      appendResponseBodyPart(response);
+    }
+    writer.append("--").append(boundary).append("--").append(LF).append(LF);
+  }
+
+  private void appendResponseBody(final List<BatchResponsePart> batchResponseParts, final String boundary) throws BatchException {
+
+    for (BatchResponsePart batchResponsePart : batchResponseParts) {
+      writer.append("--").append(boundary).append(LF);
+      if (batchResponsePart.isChangeSet()) {
+        appendChangeSet(batchResponsePart);
+      } else {
+        ODataResponse response = batchResponsePart.getResponses().get(0);
+        appendResponseBodyPart(response);
+      }
+    }
+    writer.append("--").append(boundary).append("--");
+  }
+
+  private void appendResponseBodyPart(final ODataResponse response) throws BatchException {
+    writer.append(HttpHeaders.CONTENT_TYPE).append(COLON).append(SP).append(BatchConstants.HTTP_APPLICATION_HTTP).append(LF);
+    writer.append(BatchConstants.HTTP_CONTENT_TRANSFER_ENCODING).append(COLON).append(SP).append("binary").append(LF);
+    if (response.getHeader(BatchConstants.MIME_HEADER_CONTENT_ID) != null) {
+      writer.append(BatchConstants.HTTP_CONTENT_ID).append(COLON).append(SP).append(response.getHeader(BatchConstants.MIME_HEADER_CONTENT_ID)).append(LF);
+    }
+    writer.append(LF);
+    writer.append("HTTP/1.1").append(SP).append(response.getStatus().getStatusCode()).append(SP).append(response.getStatus().getInfo()).append(LF);
+    appendHeader(response);
+    if (!HttpStatusCodes.NO_CONTENT.equals(response.getStatus())) {
+      String body;
+      if (response.getEntity() instanceof InputStream) {
+        InputStream in = (InputStream) response.getEntity();
+        body = readBody(in);
+      } else {
+        body = response.getEntity().toString();
+      }
+      writer.append(HttpHeaders.CONTENT_LENGTH).append(COLON).append(SP).append(body.length()).append(LF).append(LF);
+      writer.append(body);
+    }
+    writer.append(LF).append(LF);
+  }
+
+  private void appendHeader(final ODataResponse response) {
+    for (String name : response.getHeaderNames()) {
+      if (!BatchConstants.MIME_HEADER_CONTENT_ID.equalsIgnoreCase(name) && !BatchConstants.REQUEST_HEADER_CONTENT_ID.equalsIgnoreCase(name)) {
+        writer.append(name).append(COLON).append(SP).append(response.getHeader(name)).append(LF);
+      } else if (BatchConstants.REQUEST_HEADER_CONTENT_ID.equalsIgnoreCase(name)) {
+        writer.append(BatchConstants.HTTP_CONTENT_ID).append(COLON).append(SP).append(response.getHeader(name)).append(LF);
+      }
+    }
+  }
+
+  private String generateBoundary(final String value) {
+    return value + "_" + UUID.randomUUID().toString();
+  }
+
+  private String readBody(final InputStream in) throws BatchException {
+    byte[] tmp = new byte[2048];
+    int count;
+    BatchException cachedException = null;
+    StringBuffer b = new StringBuffer();
+    try {
+      count = in.read(tmp);
+      while (count >= 0) {
+        b.append(new String(tmp, 0, count));
+        count = in.read(tmp);
+      }
+    } catch (IOException e) {
+      cachedException = new BatchException(ODataMessageException.COMMON, e);
+      throw cachedException;
+    } finally {// NOPMD (suppress DoNotThrowExceptionInFinally)
+      try {
+        in.close();
+      } catch (IOException e) {
+        if (cachedException != null) {
+          throw cachedException;
+        }
+      }
+    }
+    return b.toString();
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/ff2b0a0e/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/ContentType.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/ContentType.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/ContentType.java
new file mode 100644
index 0000000..75e47f1
--- /dev/null
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/ContentType.java
@@ -0,0 +1,734 @@
+/*******************************************************************************
+ * 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.odata2.core.commons;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * Internally used {@link ContentType} for OData library.
+ * 
+ * For more details on format and content of a {@link ContentType} see    
+ * <code>Media Type</code> format as defined in <code>RFC 2616 chapter 3.7 (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html)</code>.
+ * <pre>
+ * <code>
+ *   media-type     = type "/" subtype *( ";" parameter )
+ *   type           = token
+ *   subtype        = token
+ * </code>
+ * </pre>
+ * 
+ * Especially for <code>Accept</code> Header as defined in 
+ * <code>RFC 2616 chapter 14.1 (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html)</code>:
+ * <pre>
+ * <code>
+ * Accept = "Accept" ":"
+ *          #( media-range [ accept-params ] )
+ *  media-range = ( "* /*"
+ *                | ( type "/" "*" )
+ *                | ( type "/" subtype )
+ *                ) *( ";" parameter )
+ *  accept-params  = ";" "q" "=" qvalue *( accept-extension )
+ *  accept-extension = ";" token [ "=" ( token | quoted-string ) ]
+ * </code>
+ * </pre>
+ * 
+ * Especially for <code>Content-Type</code> Header as defined in 
+ * <code>RFC 2616 chapter 14.7 (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html)</code>:
+ * <pre>
+ * <code>
+ * Content-Type   = "Content-Type" ":" media-type
+ * </code>
+ * </pre>
+ * 
+ * Once created a {@link ContentType} is <b>IMMUTABLE</b>.
+ * 
+ * @author SAP AG
+ */
+public class ContentType {
+
+  public enum ODataFormat {
+    ATOM, XML, JSON, MIME, CUSTOM
+  }
+
+  private static final Set<String> KNOWN_MIME_TYPES = new HashSet<String>();
+  static {
+    KNOWN_MIME_TYPES.add("audio");
+    KNOWN_MIME_TYPES.add("image");
+    KNOWN_MIME_TYPES.add("video");
+    KNOWN_MIME_TYPES.add("multipart");
+    KNOWN_MIME_TYPES.add("text");
+  }
+
+  private static final char WHITESPACE_CHAR = ' ';
+  private static final String PARAMETER_SEPARATOR = ";";
+  private static final String TYPE_SUBTYPE_SEPARATOR = "/";
+  private static final String MEDIA_TYPE_WILDCARD = "*";
+  private static final String VERBOSE = "verbose";
+
+  public static final String PARAMETER_CHARSET = "charset";
+  public static final String PARAMETER_ODATA = "odata";
+  public static final String PARAMETER_Q = "q";
+  public static final String CHARSET_UTF_8 = "utf-8";
+
+  public static final ContentType WILDCARD = new ContentType(MEDIA_TYPE_WILDCARD, MEDIA_TYPE_WILDCARD);
+
+  public static final ContentType APPLICATION_XML = new ContentType("application", "xml", ODataFormat.XML);
+  public static final ContentType APPLICATION_XML_CS_UTF_8 = ContentType.create(APPLICATION_XML, PARAMETER_CHARSET, CHARSET_UTF_8);
+  public static final ContentType APPLICATION_ATOM_XML = new ContentType("application", "atom+xml", ODataFormat.ATOM);
+  public static final ContentType APPLICATION_ATOM_XML_CS_UTF_8 = ContentType.create(APPLICATION_ATOM_XML, PARAMETER_CHARSET, CHARSET_UTF_8);
+  public static final ContentType APPLICATION_ATOM_XML_ENTRY = new ContentType("application", "atom+xml", ODataFormat.ATOM, parameterMap("type", "entry"));
+  public static final ContentType APPLICATION_ATOM_XML_ENTRY_CS_UTF_8 = ContentType.create(APPLICATION_ATOM_XML_ENTRY, PARAMETER_CHARSET, CHARSET_UTF_8);
+  public static final ContentType APPLICATION_ATOM_XML_FEED = new ContentType("application", "atom+xml", ODataFormat.ATOM, parameterMap("type", "feed"));
+  public static final ContentType APPLICATION_ATOM_XML_FEED_CS_UTF_8 = ContentType.create(APPLICATION_ATOM_XML_FEED, PARAMETER_CHARSET, CHARSET_UTF_8);
+  public static final ContentType APPLICATION_ATOM_SVC = new ContentType("application", "atomsvc+xml", ODataFormat.ATOM);
+  public static final ContentType APPLICATION_ATOM_SVC_CS_UTF_8 = ContentType.create(APPLICATION_ATOM_SVC, PARAMETER_CHARSET, CHARSET_UTF_8);
+  public static final ContentType APPLICATION_JSON = new ContentType("application", "json", ODataFormat.JSON);
+  public static final ContentType APPLICATION_JSON_ODATA_VERBOSE = ContentType.create(APPLICATION_JSON, PARAMETER_ODATA, VERBOSE);
+  public static final ContentType APPLICATION_JSON_CS_UTF_8 = ContentType.create(APPLICATION_JSON, PARAMETER_CHARSET, CHARSET_UTF_8);
+  public static final ContentType APPLICATION_OCTET_STREAM = new ContentType("application", "octet-stream");
+  public static final ContentType TEXT_PLAIN = new ContentType("text", "plain");
+  public static final ContentType TEXT_PLAIN_CS_UTF_8 = ContentType.create(TEXT_PLAIN, PARAMETER_CHARSET, CHARSET_UTF_8);
+  public static final ContentType MULTIPART_MIXED = new ContentType("multipart", "mixed");
+
+  private String type;
+  private String subtype;
+  private Map<String, String> parameters;
+  private ODataFormat odataFormat;
+
+  private ContentType(final String type, final String subtype) {
+    this(type, subtype, ODataFormat.CUSTOM, null);
+  }
+
+  private ContentType(final String type, final String subtype, final ODataFormat odataFormat) {
+    this(type, subtype, odataFormat, null);
+  }
+
+  private ContentType(final String type, final String subtype, final ODataFormat odataFormat, final Map<String, String> parameters) {
+    if ((type == null || MEDIA_TYPE_WILDCARD.equals(type)) && !MEDIA_TYPE_WILDCARD.equals(subtype)) {
+      throw new IllegalArgumentException("Illegal combination of WILDCARD type with NONE WILDCARD subtype.");
+    }
+    this.odataFormat = odataFormat;
+    this.type = validateType(type);
+    this.subtype = validateType(subtype);
+
+    if (parameters == null) {
+      this.parameters = Collections.emptyMap();
+    } else {
+      this.parameters = new TreeMap<String, String>(new Comparator<String>() {
+        @Override
+        public int compare(final String o1, final String o2) {
+          return o1.compareToIgnoreCase(o2);
+        }
+      });
+      this.parameters.putAll(parameters);
+      this.parameters.remove(PARAMETER_Q);
+    }
+  }
+
+  private String validateType(final String type) {
+    if (type == null || type.isEmpty()) {
+      return MEDIA_TYPE_WILDCARD;
+    }
+    if (type.charAt(0) == WHITESPACE_CHAR || type.charAt(type.length() - 1) == WHITESPACE_CHAR) {
+      throw new IllegalArgumentException("Illegal leading/trailing whitespace found for type '" + type + "'.");
+    }
+    return type;
+  }
+
+  /**
+   * Validates if given <code>format</code> is parseable and can be used as input for
+   * {@link #create(String)} method.
+   * @param format to be validated string
+   * @return <code>true</code> if format is parseable otherwise <code>false</code>
+   */
+  public static boolean isParseable(final String format) {
+    try {
+      return ContentType.create(format) != null;
+    } catch (IllegalArgumentException e) {
+      return false;
+    }
+  }
+
+  /**
+   * Creates a content type from type and subtype
+   * @param type
+   * @param subtype
+   * @return a new <code>ContentType</code> object
+   */
+  public static ContentType create(final String type, final String subtype) {
+    return new ContentType(type, subtype, mapToODataFormat(type, subtype), null);
+  }
+
+  /**
+   * 
+   * @param type
+   * @param subtype
+   * @param parameters
+   * @return a new <code>ContentType</code> object
+   */
+  public static ContentType create(final String type, final String subtype, final Map<String, String> parameters) {
+    return new ContentType(type, subtype, mapToODataFormat(type, subtype), parameters);
+  }
+
+  /**
+   * 
+   * @param contentType
+   * @param parameterKey
+   * @param parameterValue
+   * @return a new <code>ContentType</code> object
+   */
+  public static ContentType create(final ContentType contentType, final String parameterKey, final String parameterValue) {
+    ContentType ct = new ContentType(contentType.type, contentType.subtype, contentType.odataFormat, contentType.parameters);
+    ct.parameters.put(parameterKey, parameterValue);
+    return ct;
+  }
+
+  /**
+   * Create a {@link ContentType} based on given input string (<code>format</code>).
+   * 
+   * Supported format is <code>Media Type</code> format as defined in <code>RFC 2616 chapter 3.7</code>.
+   * This format is used as
+   * <code>HTTP Accept HEADER</code> format as defined in <code>RFC 2616 chapter 14.1</code>
+   * and 
+   * <code>HTTP Content-Type HEADER</code> format as defined in <code>RFC 2616 chapter 14.17</code>
+   * 
+   * @param format a string in format as defined in <code>RFC 2616 section 3.7</code>
+   * @return a new <code>ContentType</code> object
+   * @throws IllegalArgumentException if input string is not parseable
+   */
+  public static ContentType create(final String format) {
+    if (format == null) {
+      throw new IllegalArgumentException("Parameter format MUST NOT be NULL.");
+    }
+
+    // split 'types' and 'parameters'
+    String[] typesAndParameters = format.split(PARAMETER_SEPARATOR, 2);
+    String types = typesAndParameters[0];
+    String parameters = (typesAndParameters.length > 1 ? typesAndParameters[1] : null);
+    //
+    Map<String, String> parametersMap = parseParameters(parameters);
+    //
+    if (types.contains(TYPE_SUBTYPE_SEPARATOR)) {
+      String[] tokens = types.split(TYPE_SUBTYPE_SEPARATOR);
+      if (tokens.length == 2) {
+        return create(tokens[0], tokens[1], parametersMap);
+      } else {
+        throw new IllegalArgumentException("Too many '" + TYPE_SUBTYPE_SEPARATOR + "' in format '" + format + "'.");
+      }
+    } else {
+      return create(types, MEDIA_TYPE_WILDCARD, parametersMap);
+    }
+  }
+
+  /**
+   * Create a list of {@link ContentType} based on given input strings (<code>contentTypes</code>).
+   * 
+   * Supported format is <code>Media Type</code> format as defined in <code>RFC 2616 chapter 3.7</code>.
+   * This format is used as
+   * <code>HTTP Accept HEADER</code> format as defined in <code>RFC 2616 chapter 14.1</code>
+   * and 
+   * <code>HTTP Content-Type HEADER</code> format as defined in <code>RFC 2616 chapter 14.17</code>.
+   * <p>
+   * If one of the given strings can not be parsed an exception is thrown (hence no list is returned with the parseable strings).
+   * </p>
+   * 
+   * @param contentTypeStrings a list of strings in format as defined in <code>RFC 2616 section 3.7</code>
+   * @return a list of new <code>ContentType</code> object
+   * @throws IllegalArgumentException if one of the given input string is not parseable this exceptions is thrown
+   */
+  public static List<ContentType> create(final List<String> contentTypeStrings) {
+    List<ContentType> contentTypes = new ArrayList<ContentType>(contentTypeStrings.size());
+    for (String contentTypeString : contentTypeStrings) {
+      contentTypes.add(create(contentTypeString));
+    }
+    return contentTypes;
+  }
+
+  /**
+   * Parses the given input string (<code>format</code>) and returns created
+   * {@link ContentType} if input was valid or return <code>NULL</code> if
+   * input was not parseable.
+   * 
+   * For the definition of the supported format see {@link #create(String)}.
+   * 
+   * @param format a string in format as defined in <code>RFC 2616 section 3.7</code>
+   * @return a new <code>ContentType</code> object
+   */
+  public static ContentType parse(final String format) {
+    try {
+      return ContentType.create(format);
+    } catch (IllegalArgumentException e) {
+      return null;
+    }
+  }
+
+  /**
+   * 
+   * @param subtype
+   * @return
+   */
+  private static ODataFormat mapToODataFormat(final String type, final String subtype) {
+    ODataFormat odataFormat = ODataFormat.CUSTOM;
+    if (type.contains("application")) {
+      if (subtype.contains("atom")) {
+        odataFormat = ODataFormat.ATOM;
+      } else if (subtype.contains("xml")) {
+        odataFormat = ODataFormat.XML;
+      } else if (subtype.contains("json")) {
+        odataFormat = ODataFormat.JSON;
+      }
+    } else if (KNOWN_MIME_TYPES.contains(type)) {
+      odataFormat = ODataFormat.MIME;
+    }
+    return odataFormat;
+  }
+
+  /**
+   * 
+   * @param content
+   * @return a new <code>ContentType</code> object
+   */
+  private static Map<String, String> parameterMap(final String... content) {
+    Map<String, String> map = new HashMap<String, String>();
+    for (int i = 0; i < content.length - 1; i += 2) {
+      String key = content[i];
+      String value = content[i + 1];
+      map.put(key, value);
+    }
+    return map;
+  }
+
+  /**
+   * Valid input are <code>;</code> separated <code>key=value</code> pairs 
+   * without spaces between key and value.
+   * 
+   * <p>
+   * See RFC 2616:
+   * The type, subtype, and parameter attribute names are case-insensitive. 
+   * Parameter values might or might not be case-sensitive, depending on the 
+   * semantics of the parameter name. <b>Linear white space (LWS) MUST NOT be used 
+   * between the type and subtype, nor between an attribute and its value</b>. 
+   * </p>
+   * 
+   * @param parameters
+   * @return Map with keys mapped to values
+   */
+  private static Map<String, String> parseParameters(final String parameters) {
+    Map<String, String> parameterMap = new HashMap<String, String>();
+    if (parameters != null) {
+      String[] splittedParameters = parameters.split(PARAMETER_SEPARATOR);
+      for (String parameter : splittedParameters) {
+        String[] keyValue = parameter.split("=");
+        String key = keyValue[0].trim().toLowerCase(Locale.ENGLISH);
+        if (isParameterAllowed(key)) {
+          String value = keyValue.length > 1 ? keyValue[1] : null;
+          if (value != null && isLws(value.charAt(0))) {
+            throw new IllegalArgumentException("Value of parameter '" + key + "' starts with a LWS ('" + parameters + "').");
+          }
+          parameterMap.put(key, value);
+        }
+      }
+    }
+    return parameterMap;
+  }
+
+  /** 
+   * Validate if given character is a linear whitepace (includes <code>horizontal-tab, linefeed, carriage return and space</code>).
+   * 
+   * @param character to be checked
+   * @return <code>true</code> if character is a LWS, otherwise <code>false</code>.
+   */
+  private static boolean isLws(final char character) {
+    switch (character) {
+    case 9: // HT = <US-ASCII HT, horizontal-tab (9)>
+    case 10: // LF = <US-ASCII LF, linefeed (10)>
+    case 13: // CR = <US-ASCII CR, carriage return (13)>
+    case 32: // SP = <US-ASCII SP, space (32)>
+      return true;
+    default:
+      return false;
+    }
+  }
+
+  private static boolean isParameterAllowed(final String key) {
+    return key != null && !PARAMETER_Q.equals(key.toLowerCase(Locale.US));
+  }
+
+  /**
+   * Ensure that charset parameter ({@link #PARAMETER_CHARSET}) is set on returned content type
+   * if this {@link ContentType} is a <code>odata text related</code> content type (@see {@link #isContentTypeODataTextRelated()}).
+   * If <code>this</code> {@link ContentType} has no charset parameter set a new {@link ContentType}
+   * with given <code>defaultCharset</code> is created.
+   * Otherwise if charset parameter is already set nothing is done.
+   * 
+   * @param defaultCharset
+   * @return ContentType
+   */
+  public ContentType receiveWithCharsetParameter(final String defaultCharset) {
+    if (isContentTypeODataTextRelated()) {
+      if (!parameters.containsKey(ContentType.PARAMETER_CHARSET)) {
+        return ContentType.create(this, ContentType.PARAMETER_CHARSET, defaultCharset);
+      }
+    }
+    return this;
+  }
+
+  /**
+   * 
+   * @return <code>true</code> if this {@link ContentType} is text related (in the view of OData)
+   */
+  public boolean isContentTypeODataTextRelated() {
+    return (ContentType.TEXT_PLAIN.equals(this)
+        || (getODataFormat() == ODataFormat.XML)
+        || (getODataFormat() == ODataFormat.ATOM)
+        || (getODataFormat() == ODataFormat.JSON));
+  }
+
+  public String getType() {
+    return type;
+  }
+
+  public String getSubtype() {
+    return subtype;
+  }
+
+  /**
+   * 
+   * @return parameters of this {@link ContentType} as unmodifiable map.
+   */
+  public Map<String, String> getParameters() {
+    return Collections.unmodifiableMap(parameters);
+  }
+
+  @Override
+  public int hashCode() {
+    return 1;
+  }
+
+  /**
+   * {@link ContentType}s are equal 
+   * <ul>
+   * <li>if <code>type</code>, <code>subtype</code> and all <code>parameters</code> have the same value.</li>
+   * <li>if <code>type</code> and/or <code>subtype</code> is set to "*" (in such a case the <code>parameters</code> are ignored).</li>
+   * </ul>
+   * 
+   * @return <code>true</code> if both instances are equal (see definition above), otherwise <code>false</code>.
+   */
+  @Override
+  public boolean equals(final Object obj) {
+    // NULL validation is done in method 'isEqualWithoutParameters(obj)'
+    Boolean compatible = isEqualWithoutParameters(obj);
+
+    if (compatible == null) {
+      ContentType other = (ContentType) obj;
+
+      // parameter checks
+      if (parameters == null) {
+        if (other.parameters != null) {
+          return false;
+        }
+      } else if (parameters.size() == other.parameters.size()) {
+        Iterator<Entry<String, String>> entries = parameters.entrySet().iterator();
+        Iterator<Entry<String, String>> otherEntries = other.parameters.entrySet().iterator();
+        while (entries.hasNext()) {
+          Entry<String, String> e = entries.next();
+          Entry<String, String> oe = otherEntries.next();
+
+          if (!areEqual(e.getKey(), oe.getKey())) {
+            return false;
+          }
+          if (!areEqual(e.getValue(), oe.getValue())) {
+            return false;
+          }
+        }
+      } else {
+        return false;
+      }
+      return true;
+    } else {
+      // all tests run
+      return compatible.booleanValue();
+    }
+  }
+
+  /**
+   * {@link ContentType}s are <b>compatible</b> 
+   * <ul>
+   * <li>if <code>type</code>, <code>subtype</code> have the same value.</li>
+   * <li>if <code>type</code> and/or <code>subtype</code> is set to "*"</li>
+   * </ul>
+   * The set <code>parameters</code> are <b>always</b> ignored (for compare with parameters see {@link #equals(Object)}).
+   * 
+   * @return <code>true</code> if both instances are equal (see definition above), otherwise <code>false</code>.
+   */
+  public boolean isCompatible(final ContentType obj) {
+    Boolean compatible = isEqualWithoutParameters(obj);
+    if (compatible == null) {
+      return true;
+    }
+    return compatible.booleanValue();
+  }
+
+  /**
+   * Check equal without parameters.
+   * It is possible that no decision about <code>equal/none equal</code> can be determined a <code>NULL</code> is returned.
+   * 
+   * @param obj to checked object
+   * @return <code>true</code> if both instances are equal (see definition above), otherwise <code>false</code> 
+   *          or <code>NULL</code> if no decision about <code>equal/none equal</code> could be determined.
+   */
+  private Boolean isEqualWithoutParameters(final Object obj) {
+    // basic checks
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null) {
+      return false;
+    }
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
+
+    ContentType other = (ContentType) obj;
+
+    // subtype checks
+    if (subtype == null) {
+      if (other.subtype != null) {
+        return false;
+      }
+    } else if (!subtype.equals(other.subtype)) {
+      if (!subtype.equals(MEDIA_TYPE_WILDCARD) && !other.subtype.equals(MEDIA_TYPE_WILDCARD)) {
+        return false;
+      }
+    }
+
+    // type checks
+    if (type == null) {
+      if (other.type != null) {
+        return false;
+      }
+    } else if (!type.equals(other.type)) {
+      if (!type.equals(MEDIA_TYPE_WILDCARD) && !other.type.equals(MEDIA_TYPE_WILDCARD)) {
+        return false;
+      }
+    }
+
+    // if wildcards are set, content types are defined as 'equal'
+    if (countWildcards() > 0 || other.countWildcards() > 0) {
+      return true;
+    }
+
+    return null;
+  }
+
+  /**
+   * Check whether both string are equal ignoring the case of the strings.
+   * 
+   * @param first
+   * @param second
+   * @return
+   */
+  private static boolean areEqual(final String first, final String second) {
+    if (first == null) {
+      if (second != null) {
+        return false;
+      }
+    } else if (!first.equalsIgnoreCase(second)) {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Get {@link ContentType} as string as defined in RFC 2616 (http://www.ietf.org/rfc/rfc2616.txt - chapter 14.17: Content-Type)
+   * 
+   * @return string representation of <code>ContentType</code> object
+   */
+  public String toContentTypeString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(type).append(TYPE_SUBTYPE_SEPARATOR).append(subtype);
+    for (String key : parameters.keySet()) {
+      if (isParameterAllowed(key)) {
+        String value = parameters.get(key);
+        sb.append(";").append(key).append("=").append(value);
+      }
+    }
+    return sb.toString();
+  }
+
+  @Override
+  public String toString() {
+    return toContentTypeString();
+  }
+
+  public ODataFormat getODataFormat() {
+    return odataFormat;
+  }
+
+  /**
+   * Find best match between this {@link ContentType} and the {@link ContentType} in the list.
+   * If a match (this {@link ContentType} is equal to a {@link ContentType} in list) is found either this or the {@link ContentType}
+   * from the list is returned based on which {@link ContentType} has less "**" characters set 
+   * (checked with {@link #compareWildcardCounts(ContentType)}.
+   * If no match (none {@link ContentType} in list is equal to this {@link ContentType}) is found <code>NULL</code> is returned.
+   * 
+   * @param toMatchContentTypes list of {@link ContentType}s which are matches against this {@link ContentType}
+   * @return best matched content type in list or <code>NULL</code> if none content type match to this content type instance
+   */
+  public ContentType match(final List<ContentType> toMatchContentTypes) {
+    for (ContentType supportedContentType : toMatchContentTypes) {
+      if (equals(supportedContentType)) {
+        if (compareWildcardCounts(supportedContentType) < 0) {
+          return this;
+        } else {
+          return supportedContentType;
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Find best match between this {@link ContentType} and the {@link ContentType} in the list ignoring all set parameters.
+   * If a match (this {@link ContentType} is equal to a {@link ContentType} in list) is found either this or the {@link ContentType}
+   * from the list is returned based on which {@link ContentType} has less "**" characters set 
+   * (checked with {@link #compareWildcardCounts(ContentType)}.
+   * If no match (none {@link ContentType} in list is equal to this {@link ContentType}) is found <code>NULL</code> is returned.
+   * 
+   * @param toMatchContentTypes list of {@link ContentType}s which are matches against this {@link ContentType}
+   * @return best matched content type in list or <code>NULL</code> if none content type match to this content type instance
+   */
+  public ContentType matchCompatible(final List<ContentType> toMatchContentTypes) {
+    for (ContentType supportedContentType : toMatchContentTypes) {
+      if (isCompatible(supportedContentType)) {
+        if (compareWildcardCounts(supportedContentType) < 0) {
+          return this;
+        } else {
+          return supportedContentType;
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Check if a valid compatible match for this {@link ContentType} exists in given list.
+   * Compatible in this case means that <b>all set parameters are ignored</b>.
+   * For more detail what a valid match is see {@link #matchCompatible(List)}.
+   * 
+   * @param toMatchContentTypes list of {@link ContentType}s which are matches against this {@link ContentType}
+   * @return <code>true</code> if a compatible content type was found in given list 
+   *          or <code>false</code> if none compatible content type match was found
+   */
+  public boolean hasCompatible(final List<ContentType> toMatchContentTypes) {
+    return matchCompatible(toMatchContentTypes) != null;
+  }
+
+  /**
+   * Check if a valid match for this {@link ContentType} exists in given list.
+   * For more detail what a valid match is see {@link #match(List)}.
+   * 
+   * @param toMatchContentTypes list of {@link ContentType}s which are matches against this {@link ContentType}
+   * @return <code>true</code> if a matching content type was found in given list 
+   *          or <code>false</code> if none matching content type match was found
+   */
+  public boolean hasMatch(final List<ContentType> toMatchContentTypes) {
+    return match(toMatchContentTypes) != null;
+  }
+
+  /**
+   * Compare wildcards counts/weights of both {@link ContentType}.
+   * 
+   * The smaller {@link ContentType} has lesser weighted wildcards then the bigger {@link ContentType}.
+   * As result this method returns this object weighted wildcards minus the given parameter object weighted wildcards.
+   * 
+   * A type wildcard is weighted with <code>2</code> and a subtype wildcard is weighted with <code>1</code>.
+   * 
+   * @param otherContentType {@link ContentType} to be compared to
+   * @return this object weighted wildcards minus the given parameter object weighted wildcards.
+   */
+  public int compareWildcardCounts(final ContentType otherContentType) {
+    return countWildcards() - otherContentType.countWildcards();
+  }
+
+  private int countWildcards() {
+    int count = 0;
+    if (MEDIA_TYPE_WILDCARD.equals(type)) {
+      count += 2;
+    }
+    if (MEDIA_TYPE_WILDCARD.equals(subtype)) {
+      count++;
+    }
+    return count;
+  }
+
+  /**
+   * 
+   * @return <code>true</code> if <code>type</code> or <code>subtype</code> of this instance is a "*".
+   */
+  public boolean hasWildcard() {
+    return (MEDIA_TYPE_WILDCARD.equals(type) || MEDIA_TYPE_WILDCARD.equals(subtype));
+  }
+
+  /**
+   * 
+   * @return <code>true</code> if both <code>type</code> and <code>subtype</code> of this instance are a "*".
+   */
+  public boolean isWildcard() {
+    return (MEDIA_TYPE_WILDCARD.equals(type) && MEDIA_TYPE_WILDCARD.equals(subtype));
+  }
+
+  public static List<ContentType> convert(final List<String> types) {
+    List<ContentType> results = new ArrayList<ContentType>();
+    for (String contentType : types) {
+      results.add(ContentType.create(contentType));
+    }
+    return results;
+  }
+
+  /**
+   * Check if a valid match for given content type formated string (<code>toMatch</code>) exists in given list.
+   * Therefore the given content type formated string (<code>toMatch</code>) is converted into a {@link ContentType}
+   * with a simple {@link #create(String)} call (during which an exception can occur).
+   * 
+   * For more detail in general see {@link #hasMatch(List)} and for what a valid match is see {@link #match(List)}.
+   * 
+   * @param toMatch content type formated string (<code>toMatch</code>) for which is checked if a match exists in given list
+   * @param matchExamples list of {@link ContentType}s which are matches against content type formated string (<code>toMatch</code>)
+   * @return <code>true</code> if a matching content type was found in given list 
+   *          or <code>false</code> if none matching content type match was found
+   */
+  public static boolean match(final String toMatch, final ContentType... matchExamples) {
+    ContentType toMatchContentType = ContentType.create(toMatch);
+
+    return toMatchContentType.hasMatch(Arrays.asList(matchExamples));
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/ff2b0a0e/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/Decoder.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/Decoder.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/Decoder.java
new file mode 100644
index 0000000..358faec
--- /dev/null
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/Decoder.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * 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.odata2.core.commons;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Decodes a Java String containing a percent-encoded UTF-8 String value
+ * into a Java String (in its internal UTF-16 encoding).
+ * @author SAP AG
+ */
+public class Decoder {
+
+  /**
+   * Decodes a percent-encoded UTF-8 String value into a Java String
+   * (in its internal UTF-16 encoding).
+   * @param value the encoded String
+   * @return the Java String
+   * @throws IllegalArgumentException if value contains characters not representing UTF-8 bytes
+   *                                  or ends with an unfinished percent-encoded character
+   * @throws NumberFormatException    if the two characters after a percent character
+   *                                  are not hexadecimal digits
+   */
+  public static String decode(final String value) throws IllegalArgumentException, NumberFormatException {
+    if (value == null) {
+      return value;
+    }
+
+    // Use a tiny finite-state machine to handle decoding on byte level.
+    // There are only three states:
+    //   -2: normal bytes
+    //   -1: a byte representing the percent character has been read
+    // >= 0: a byte representing the first half-byte of a percent-encoded byte has been read
+    // The variable holding the state is also used to store the value of the first half-byte.
+    byte[] result = new byte[value.length()];
+    int position = 0;
+    byte encodedPart = -2;
+    for (final char c : value.toCharArray()) {
+      if (c <= Byte.MAX_VALUE) {
+        if (c == '%') {
+          if (encodedPart == -2) {
+            encodedPart = -1;
+          } else {
+            throw new IllegalArgumentException();
+          }
+        } else if (encodedPart == -1) {
+          encodedPart = (byte) c;
+        } else if (encodedPart >= 0) {
+          final int i = Integer.parseInt(String.valueOf(new char[] { (char) encodedPart, c }), 16);
+          if (i >= 0) {
+            result[position++] = (byte) i;
+          } else {
+            throw new NumberFormatException();
+          }
+          encodedPart = -2;
+        } else {
+          result[position++] = (byte) c;
+        }
+      } else {
+        throw new IllegalArgumentException();
+      }
+    }
+
+    if (encodedPart >= 0) {
+      throw new IllegalArgumentException();
+    }
+
+    try {
+      return new String(result, 0, position, "UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/ff2b0a0e/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/Encoder.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/Encoder.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/Encoder.java
new file mode 100644
index 0000000..1e28c8e
--- /dev/null
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/Encoder.java
@@ -0,0 +1,149 @@
+/*******************************************************************************
+ * 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.odata2.core.commons;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Encodes a Java String (in its internal UTF-16 encoding) into its
+ * percent-encoded UTF-8 representation according to
+ * <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>
+ * (with consideration of its predecessor RFC 2396).
+ * @author SAP AG
+ */
+public class Encoder {
+
+  /**
+   * Encodes a Java String (in its internal UTF-16 encoding) into its
+   * percent-encoded UTF-8 representation according to
+   * <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>,
+   * suitable for parts of an OData path segment.
+   * @param value the Java String
+   * @return the encoded String
+   */
+  public static String encode(final String value) {
+    return encoder.encodeInternal(value);
+  }
+
+  // OData has special handling for "'", so we allow that to remain unencoded.
+  // Other sub-delims not used neither by JAX-RS nor by OData could be added
+  // if the encoding is considered to be too aggressive.
+  // RFC 3986 would also allow the gen-delims ":" and "@" to appear literally
+  // in path-segment parts.
+  private static final String ODATA_UNENCODED = "'";
+
+  // Character classes from RFC 3986
+  private final static String UNRESERVED = "-._~"; // + ALPHA + DIGIT
+  private final static String GEN_DELIMS = ":/?#[]@";
+  // RFC 3986 says: "For consistency, URI producers and normalizers should
+  // use uppercase hexadecimal digits for all percent-encodings."
+  private final static String[] hex = {
+      "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
+      "%08", "%09", "%0A", "%0B", "%0C", "%0D", "%0E", "%0F",
+      "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
+      "%18", "%19", "%1A", "%1B", "%1C", "%1D", "%1E", "%1F",
+      "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27",
+      "%28", "%29", "%2A", "%2B", "%2C", "%2D", "%2E", "%2F",
+      "%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37",
+      "%38", "%39", "%3A", "%3B", "%3C", "%3D", "%3E", "%3F",
+      "%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47",
+      "%48", "%49", "%4A", "%4B", "%4C", "%4D", "%4E", "%4F",
+      "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57",
+      "%58", "%59", "%5A", "%5B", "%5C", "%5D", "%5E", "%5F",
+      "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67",
+      "%68", "%69", "%6A", "%6B", "%6C", "%6D", "%6E", "%6F",
+      "%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77",
+      "%78", "%79", "%7A", "%7B", "%7C", "%7D", "%7E", "%7F",
+      "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
+      "%88", "%89", "%8A", "%8B", "%8C", "%8D", "%8E", "%8F",
+      "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
+      "%98", "%99", "%9A", "%9B", "%9C", "%9D", "%9E", "%9F",
+      "%A0", "%A1", "%A2", "%A3", "%A4", "%A5", "%A6", "%A7",
+      "%A8", "%A9", "%AA", "%AB", "%AC", "%AD", "%AE", "%AF",
+      "%B0", "%B1", "%B2", "%B3", "%B4", "%B5", "%B6", "%B7",
+      "%B8", "%B9", "%BA", "%BB", "%BC", "%BD", "%BE", "%BF",
+      "%C0", "%C1", "%C2", "%C3", "%C4", "%C5", "%C6", "%C7",
+      "%C8", "%C9", "%CA", "%CB", "%CC", "%CD", "%CE", "%CF",
+      "%D0", "%D1", "%D2", "%D3", "%D4", "%D5", "%D6", "%D7",
+      "%D8", "%D9", "%DA", "%DB", "%DC", "%DD", "%DE", "%DF",
+      "%E0", "%E1", "%E2", "%E3", "%E4", "%E5", "%E6", "%E7",
+      "%E8", "%E9", "%EA", "%EB", "%EC", "%ED", "%EE", "%EF",
+      "%F0", "%F1", "%F2", "%F3", "%F4", "%F5", "%F6", "%F7",
+      "%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF"
+  };
+
+  private static final Encoder encoder = new Encoder(ODATA_UNENCODED);
+
+  /** characters to remain unencoded in addition to {@link #UNRESERVED} */
+  private final String unencoded;
+
+  private Encoder(final String unencoded) {
+    this.unencoded = unencoded == null ? "" : unencoded;
+  }
+
+  /**
+   * <p>Returns the percent-encoded UTF-8 representation of a String.</p>
+   * <p>In order to avoid producing percent-encoded CESU-8 (as described in
+   * the Unicode Consortium's <a href="http://www.unicode.org/reports/tr26/">
+   * Technical Report #26</a>), this is done in two steps:
+   * <ol>
+   * <li>Re-encode the characters from their Java-internal UTF-16 representations
+   * into their UTF-8 representations.</li>
+   * <li>Percent-encode each of the bytes in the UTF-8 representation.
+   * This is possible on byte level because all characters that do not have
+   * a <code>%xx</code> representation are represented in one byte in UTF-8.</li>
+   * </ol></p>
+   * @param input input String
+   * @return encoded representation
+   */
+  private String encodeInternal(final String input) {
+    StringBuilder resultStr = new StringBuilder();
+
+    try {
+      for (byte utf8Byte : input.getBytes("UTF-8")) {
+        final char character = (char) utf8Byte;
+        if (isUnreserved(character)) {
+          resultStr.append(character);
+        } else if (isUnencoded(character)) {
+          resultStr.append(character);
+        } else if (utf8Byte >= 0) {
+          resultStr.append(hex[utf8Byte]);
+        }
+        else {
+          // case UTF-8 continuation byte
+          resultStr.append(hex[256 + utf8Byte]); // index adjusted for the usage of signed bytes
+        }
+      }
+    } catch (final UnsupportedEncodingException e) { // should never happen; UTF-8 is always there
+      return null;
+    }
+    return resultStr.toString();
+  }
+
+  private static boolean isUnreserved(final char character) {
+    return 'A' <= character && character <= 'Z' // case A..Z
+        || 'a' <= character && character <= 'z' // case a..z
+        || '0' <= character && character <= '9' // case 0..9
+        || UNRESERVED.indexOf(character) >= 0;
+  }
+
+  private boolean isUnencoded(final char character) {
+    return unencoded.indexOf(character) >= 0;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/ff2b0a0e/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfo.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfo.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfo.java
new file mode 100644
index 0000000..76ebe08
--- /dev/null
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfo.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * 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.odata2.core.debug;
+
+import java.io.IOException;
+
+import org.apache.olingo.odata2.core.ep.util.JsonStreamWriter;
+
+/**
+ * @author SAP AG
+ */
+public interface DebugInfo {
+
+  /**
+   * Gets the name of this debug information part, useful as title.
+   * @return the name
+   */
+  public String getName();
+
+  /**
+   * Appends the content of this debug information part
+   * to the given JSON stream writer.
+   * @param jsonStreamWriter a JSON stream writer
+   */
+  public void appendJson(JsonStreamWriter jsonStreamWriter) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/ff2b0a0e/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfoBody.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfoBody.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfoBody.java
new file mode 100644
index 0000000..8ff1797
--- /dev/null
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfoBody.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * 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.odata2.core.debug;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.commons.codec.binary.Base64;
+
+import org.apache.olingo.odata2.api.commons.HttpContentType;
+import org.apache.olingo.odata2.api.ep.EntityProviderException;
+import org.apache.olingo.odata2.api.processor.ODataResponse;
+import org.apache.olingo.odata2.core.ep.BasicEntityProvider;
+import org.apache.olingo.odata2.core.ep.util.JsonStreamWriter;
+
+/**
+ * @author SAP AG
+ */
+public class DebugInfoBody implements DebugInfo {
+
+  private final ODataResponse response;
+
+  public DebugInfoBody(final ODataResponse response) {
+    this.response = response;
+  }
+
+  @Override
+  public String getName() {
+    return "Body";
+  }
+
+  @Override
+  public void appendJson(final JsonStreamWriter jsonStreamWriter) throws IOException {
+    final String contentType = response.getContentHeader();
+    if (contentType.startsWith("image/")) {
+      if (response.getEntity() instanceof InputStream) {
+        jsonStreamWriter.stringValueRaw(Base64.encodeBase64String(getBinaryFromInputStream((InputStream) response.getEntity())));
+      } else if (response.getEntity() instanceof String) {
+        jsonStreamWriter.stringValueRaw(getContentString());
+      } else {
+        throw new ClassCastException("Unsupported content entity class: " + response.getEntity().getClass().getName());
+      }
+    } else if (contentType.startsWith(HttpContentType.APPLICATION_JSON)) {
+      jsonStreamWriter.unquotedValue(getContentString());
+    } else {
+      jsonStreamWriter.stringValue(getContentString());
+    }
+  }
+
+  private String getContentString() {
+    String content;
+    if (response.getEntity() instanceof String) {
+      content = (String) response.getEntity();
+    } else if (response.getEntity() instanceof InputStream) {
+      content = getStringFromInputStream((InputStream) response.getEntity());
+    } else {
+      throw new ClassCastException("Unsupported content entity class: " + response.getEntity().getClass().getName());
+    }
+    return content;
+  }
+
+  private static byte[] getBinaryFromInputStream(final InputStream inputStream) {
+    try {
+      return new BasicEntityProvider().readBinary(inputStream);
+    } catch (final EntityProviderException e) {
+      return null;
+    }
+  }
+
+  private static String getStringFromInputStream(final InputStream inputStream) {
+    try {
+      return new BasicEntityProvider().readText(inputStream);
+    } catch (final EntityProviderException e) {
+      return null;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/ff2b0a0e/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfoException.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfoException.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfoException.java
new file mode 100644
index 0000000..d94e1bf
--- /dev/null
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/debug/DebugInfoException.java
@@ -0,0 +1,94 @@
+/*******************************************************************************
+ * 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.odata2.core.debug;
+
+import java.io.IOException;
+import java.util.Locale;
+
+import org.apache.olingo.odata2.api.exception.ODataMessageException;
+import org.apache.olingo.odata2.core.ep.util.JsonStreamWriter;
+import org.apache.olingo.odata2.core.exception.MessageService;
+
+/**
+ * @author SAP AG
+ */
+public class DebugInfoException implements DebugInfo {
+
+  private final Exception exception;
+  private final Locale locale;
+
+  public DebugInfoException(final Exception exception, final Locale locale) {
+    this.exception = exception;
+    this.locale = locale;
+  }
+
+  @Override
+  public String getName() {
+    return "Stacktrace";
+  }
+
+  @Override
+  public void appendJson(final JsonStreamWriter jsonStreamWriter) throws IOException {
+    jsonStreamWriter.beginObject()
+        .name("exceptions")
+        .beginArray();
+    Throwable throwable = exception;
+    while (throwable != null) {
+      jsonStreamWriter.beginObject()
+          .namedStringValueRaw("class", throwable.getClass().getCanonicalName()).separator()
+          .namedStringValue("message",
+              throwable instanceof ODataMessageException ?
+                  MessageService.getMessage(locale, ((ODataMessageException) throwable).getMessageReference()).getText() :
+                  throwable.getLocalizedMessage())
+          .separator();
+
+      jsonStreamWriter.name("invocation");
+      appendJsonStackTraceElement(jsonStreamWriter, throwable.getStackTrace()[0]);
+
+      jsonStreamWriter.endObject();
+      throwable = throwable.getCause();
+      if (throwable != null) {
+        jsonStreamWriter.separator();
+      }
+    }
+    jsonStreamWriter.endArray();
+    jsonStreamWriter.separator();
+
+    jsonStreamWriter.name("stacktrace")
+        .beginArray();
+    boolean first = true;
+    for (final StackTraceElement stackTraceElement : exception.getStackTrace()) {
+      if (!first) {
+        jsonStreamWriter.separator();
+      }
+      first = false;
+      appendJsonStackTraceElement(jsonStreamWriter, stackTraceElement);
+    }
+    jsonStreamWriter.endArray();
+    jsonStreamWriter.endObject();
+  }
+
+  private static void appendJsonStackTraceElement(final JsonStreamWriter jsonStreamWriter, final StackTraceElement stackTraceElement) throws IOException {
+    jsonStreamWriter.beginObject()
+        .namedStringValueRaw("class", stackTraceElement.getClassName()).separator()
+        .namedStringValueRaw("method", stackTraceElement.getMethodName()).separator()
+        .name("line").unquotedValue(Integer.toString(stackTraceElement.getLineNumber()))
+        .endObject();
+  }
+}