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();
+ }
+}