From 32247295fce782a393457bc7d7a08be030d434fd Mon Sep 17 00:00:00 2001 From: Christian Holzer Date: Thu, 16 Oct 2014 17:39:39 +0200 Subject: [PATCH] [OLINGO-472] BatchParser first draft Signed-off-by: Christian Amend --- .../server/api/batch/BatchParserResult.java | 23 + .../server/api/batch/BatchRequestPart.java | 42 + .../server/api/batch/ODataResponsePart.java | 42 + .../server/core/batch/BatchException.java | 65 + .../olingo/server/core/batch/StringUtil.java | 54 + .../core/batch/parser/BatchBodyPart.java | 135 + .../core/batch/parser/BatchChangeSetPart.java | 56 + .../server/core/batch/parser/BatchParser.java | 91 + .../core/batch/parser/BatchParserCommon.java | 228 ++ .../server/core/batch/parser/BatchPart.java | 25 + .../batch/parser/BatchQueryOperation.java | 82 + .../batch/parser/BatchRequestPartImpl.java | 47 + .../BufferedReaderIncludingLineEndings.java | 286 ++ .../server/core/batch/parser/Header.java | 181 ++ .../server/core/batch/parser/HeaderField.java | 121 + .../BatchRequestTransformator.java | 191 ++ .../transformator/BatchTransformator.java | 29 + .../BatchTransformatorCommon.java | 250 ++ .../batch/writer/BatchResponseWriter.java | 228 ++ .../batch/writer/ODataResponsePartImpl.java | 44 + .../server-core-exceptions-i18n.properties | 21 + .../core/batch/BatchRequestParserTest.java | 1308 +++++++++ .../batch/parser/BatchParserCommonTest.java | 230 ++ ...ufferedReaderIncludingLineEndingsTest.java | 484 ++++ .../server/core/batch/parser/HeaderTest.java | 179 ++ .../batch/writer/BatchResponseWriterTest.java | 179 ++ .../src/test/resources/batchLarge.batch | 2422 +++++++++++++++++ .../src/test/resources/batchWithContent.batch | 1 + .../src/test/resources/batchWithPost.batch | 39 + 29 files changed, 7083 insertions(+) create mode 100644 lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchParserResult.java create mode 100644 lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchRequestPart.java create mode 100644 lib/server-api/src/main/java/org/apache/olingo/server/api/batch/ODataResponsePart.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/BatchException.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/StringUtil.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchBodyPart.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchChangeSetPart.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchParser.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchParserCommon.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchPart.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchQueryOperation.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchRequestPartImpl.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BufferedReaderIncludingLineEndings.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/Header.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/HeaderField.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchRequestTransformator.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformator.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformatorCommon.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/writer/BatchResponseWriter.java create mode 100644 lib/server-core/src/main/java/org/apache/olingo/server/core/batch/writer/ODataResponsePartImpl.java create mode 100644 lib/server-core/src/test/java/org/apache/olingo/server/core/batch/BatchRequestParserTest.java create mode 100644 lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/BatchParserCommonTest.java create mode 100644 lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/BufferedReaderIncludingLineEndingsTest.java create mode 100644 lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/HeaderTest.java create mode 100644 lib/server-core/src/test/java/org/apache/olingo/server/core/batch/writer/BatchResponseWriterTest.java create mode 100644 lib/server-core/src/test/resources/batchLarge.batch create mode 100644 lib/server-core/src/test/resources/batchWithContent.batch create mode 100644 lib/server-core/src/test/resources/batchWithPost.batch diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchParserResult.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchParserResult.java new file mode 100644 index 000000000..93bc34d23 --- /dev/null +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchParserResult.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.api.batch; + +public interface BatchParserResult { + +} diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchRequestPart.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchRequestPart.java new file mode 100644 index 000000000..ba5319fe9 --- /dev/null +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/BatchRequestPart.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.server.api.batch; + +import java.util.List; + +import org.apache.olingo.server.api.ODataRequest; + +/** + * A BatchPart + *

BatchPart represents a distinct MIME part of a Batch Request body. It can be ChangeSet or Query Operation + */ +public interface BatchRequestPart extends BatchParserResult { + + /** + * Get the info if a BatchPart is a ChangeSet + * @return true or false + */ + public boolean isChangeSet(); + + /** + * Get requests. If a BatchPart is a Query Operation, the list contains one request. + * @return a list of {@link ODataRequest} + */ + public List getRequests(); +} \ No newline at end of file diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/ODataResponsePart.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/ODataResponsePart.java new file mode 100644 index 000000000..8dd7480e5 --- /dev/null +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/batch/ODataResponsePart.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.server.api.batch; + +import java.util.List; + +import org.apache.olingo.server.api.ODataResponse; + +public interface ODataResponsePart { + /** + * Returns a collection of ODataResponses. + * Each collections contains at least one {@link ODataResponse}. + * + * If this instance represents a change set, there are may many ODataResponses + * + * @return a list of {@link ODataResponse} + */ + public List getResponses(); + + /** + * Returns true if the current instance represents a change set. + * + * @return true or false + */ + public boolean isChangeSet(); +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/BatchException.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/BatchException.java new file mode 100644 index 000000000..aafe1413a --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/BatchException.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch; + +import org.apache.olingo.server.api.ODataTranslatedException; + +public class BatchException extends ODataTranslatedException { + public static enum MessageKeys implements MessageKey { + INVALID_BOUNDARY, + INVALID_CHANGESET_METHOD, + INVALID_CONTENT, + INVALID_CONTENT_LENGTH, + INVALID_CONTENT_TRANSFER_ENCODING, + INVALID_CONTENT_TYPE, + INVALID_HEADER, + INVALID_HTTP_VERSION, + INVALID_METHOD, + INVALID_QUERY_OPERATION_METHOD, + INVALID_STATUS_LINE, + INVALID_URI, + MISSING_BLANK_LINE, + MISSING_BOUNDARY_DELIMITER, + MISSING_CLOSE_DELIMITER, + MISSING_CONTENT_ID, + MISSING_CONTENT_TRANSFER_ENCODING, + MISSING_CONTENT_TYPE, + MISSING_MANDATORY_HEADER, FORBIDDEN_HEADER; + + @Override + public String getKey() { + return name(); + } + } + + private static final long serialVersionUID = -907752788975531134L; + + public BatchException(final String developmentMessage, final MessageKey messageKey, final int lineNumber) { + this(developmentMessage, messageKey, "" + lineNumber); + } + + public BatchException(final String developmentMessage, final MessageKey messageKey, final String... parameters) { + super(developmentMessage, messageKey, parameters); + } + + @Override + protected String getBundleName() { + return DEFAULT_SERVER_BUNDLE_NAME; + } +} \ No newline at end of file diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/StringUtil.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/StringUtil.java new file mode 100644 index 000000000..2602852df --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/StringUtil.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; + +import org.apache.olingo.commons.api.ODataRuntimeException; +import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings; + +public class StringUtil { + + + public static String toString(final InputStream in) throws IOException { + final StringBuilder builder = new StringBuilder(); + final BufferedReaderIncludingLineEndings reader = new BufferedReaderIncludingLineEndings(new InputStreamReader(in)); + String currentLine; + + while((currentLine = reader.readLine()) != null) { + builder.append(currentLine); + } + + reader.close(); + + return builder.toString(); + } + + public static InputStream toInputStream(final String string) { + try { + return new ByteArrayInputStream(string.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new ODataRuntimeException("Charset UTF-8 not found"); + } + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchBodyPart.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchBodyPart.java new file mode 100644 index 000000000..c2d2e0fde --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchBodyPart.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import java.util.LinkedList; +import java.util.List; + +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.server.core.batch.BatchException; +import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings.Line; + +public class BatchBodyPart implements BatchPart { + final private String boundary; + final private boolean isStrict; + final List remainingMessage = new LinkedList(); + + private Header headers; + private boolean isChangeSet; + private List requests; + + public BatchBodyPart(final List message, final String boundary, final boolean isStrict) { + this.boundary = boundary; + this.isStrict = isStrict; + remainingMessage.addAll(message); + } + + public BatchBodyPart parse() throws BatchException { + headers = BatchParserCommon.consumeHeaders(remainingMessage); + BatchParserCommon.consumeBlankLine(remainingMessage, isStrict); + isChangeSet = isChangeSet(headers); + requests = consumeRequest(remainingMessage); + + return this; + } + + private boolean isChangeSet(final Header header) throws BatchException { + final List contentTypes = headers.getHeaders(HttpHeader.CONTENT_TYPE); + boolean isChangeSet = false; + + if (contentTypes.size() == 0) { + throw new BatchException("Missing content type", BatchException.MessageKeys.MISSING_CONTENT_TYPE, "" + + headers.getLineNumber()); + } + + for (String contentType : contentTypes) { + if (isContentTypeMultiPartMixed(contentType)) { + isChangeSet = true; + } + } + + return isChangeSet; + } + + private List consumeRequest(final List remainingMessage) throws BatchException { + if (isChangeSet) { + return consumeChangeSet(remainingMessage); + } else { + return consumeQueryOperation(remainingMessage); + } + } + + private List consumeChangeSet(final List remainingMessage2) throws BatchException { + final List> changeRequests = splitChangeSet(remainingMessage); + final List requestList = new LinkedList(); + + for (List changeRequest : changeRequests) { + requestList.add(new BatchChangeSetPart(changeRequest, isStrict).parse()); + } + + return requestList; + } + + private List> splitChangeSet(final List remainingMessage2) throws BatchException { + + final HeaderField contentTypeField = headers.getHeaderField(HttpHeader.CONTENT_TYPE); + final String changeSetBoundary = BatchParserCommon.getBoundary(contentTypeField.getValueNotNull(), + contentTypeField.getLineNumber()); + validateChangeSetBoundary(changeSetBoundary, headers); + + return BatchParserCommon.splitMessageByBoundary(remainingMessage, changeSetBoundary); + } + + private void validateChangeSetBoundary(final String changeSetBoundary, final Header header) throws BatchException { + if (changeSetBoundary.equals(boundary)) { + throw new BatchException("Change set boundary is equals to batch request boundary", + BatchException.MessageKeys.INVALID_BOUNDARY, + "" + header.getHeaderField(HttpHeader.CONTENT_TYPE).getLineNumber()); + } + } + + private List consumeQueryOperation(final List remainingMessage) throws BatchException { + final List requestList = new LinkedList(); + requestList.add(new BatchQueryOperation(remainingMessage, isStrict).parse()); + + return requestList; + } + + private boolean isContentTypeMultiPartMixed(final String contentType) { + return BatchParserCommon.PATTERN_MULTIPART_BOUNDARY.matcher(contentType).matches(); + } + + @Override + public Header getHeaders() { + return headers; + } + + @Override + public boolean isStrict() { + return isStrict; + } + + public boolean isChangeSet() { + return isChangeSet; + } + + public List getRequests() { + return requests; + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchChangeSetPart.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchChangeSetPart.java new file mode 100644 index 000000000..1d0bd6f79 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchChangeSetPart.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import java.util.List; + +import org.apache.olingo.server.core.batch.BatchException; +import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings.Line; + +public class BatchChangeSetPart extends BatchQueryOperation { + private BatchQueryOperation request; + + public BatchChangeSetPart(final List message, final boolean isStrict) throws BatchException { + super(message, isStrict); + } + + @Override + public BatchChangeSetPart parse() throws BatchException { + headers = BatchParserCommon.consumeHeaders(message); + BatchParserCommon.consumeBlankLine(message, isStrict); + + request = new BatchQueryOperation(message, isStrict).parse(); + + return this; + } + + public BatchQueryOperation getRequest() { + return request; + } + + @Override + public List getBody() { + return request.getBody(); + } + + @Override + public Line getHttpStatusLine() { + return request.getHttpStatusLine(); + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchParser.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchParser.java new file mode 100644 index 000000000..37b1a9cde --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchParser.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; + +import org.apache.olingo.commons.api.ODataRuntimeException; +import org.apache.olingo.server.api.batch.BatchParserResult; +import org.apache.olingo.server.api.batch.BatchRequestPart; +import org.apache.olingo.server.core.batch.BatchException; +import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings.Line; +import org.apache.olingo.server.core.batch.transformator.BatchRequestTransformator; + +public class BatchParser { + + private final String contentTypeMime; + private final String baseUri; + private final String rawServiceResolutionUri; + private final boolean isStrict; + + public BatchParser(final String contentType, final String baseUri, final String serviceResolutionUri, + final boolean isStrict) { + contentTypeMime = contentType; + this.baseUri = BatchParserCommon.removeEndingSlash(baseUri); + this.isStrict = isStrict; + this.rawServiceResolutionUri = serviceResolutionUri; + } + + @SuppressWarnings("unchecked") + public List parseBatchRequest(final InputStream in) throws BatchException { + return (List) parse(in, new BatchRequestTransformator(baseUri, rawServiceResolutionUri)); + } + + private List parse(final InputStream in, final BatchRequestTransformator transformator) + throws BatchException { + try { + return parseBatch(in, transformator); + } catch (IOException e) { + throw new ODataRuntimeException(e); + } finally { + try { + in.close(); + } catch (IOException e) { + throw new ODataRuntimeException(e); + } + } + } + + private List parseBatch(final InputStream in, final BatchRequestTransformator transformator) + throws IOException, BatchException { + final String boundary = BatchParserCommon.getBoundary(contentTypeMime, 1); + final List resultList = new LinkedList(); + final List> bodyPartStrings = splitBodyParts(in, boundary); + + for (List bodyPartString : bodyPartStrings) { + BatchBodyPart bodyPart = new BatchBodyPart(bodyPartString, boundary, isStrict).parse(); + resultList.addAll(transformator.transform(bodyPart)); + } + + return resultList; + } + + private List> splitBodyParts(final InputStream in, final String boundary) throws IOException, + BatchException { + final BufferedReaderIncludingLineEndings reader = new BufferedReaderIncludingLineEndings(new InputStreamReader(in)); + final List message = reader.toLineList(); + reader.close(); + + return BatchParserCommon.splitMessageByBoundary(message, boundary); + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchParserCommon.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchParserCommon.java new file mode 100644 index 000000000..e2dc5e520 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchParserCommon.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.olingo.commons.api.http.HttpContentType; +import org.apache.olingo.server.core.batch.BatchException; +import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings.Line; + +public class BatchParserCommon { + + private static final String REG_EX_BOUNDARY = + "([a-zA-Z0-9_\\-\\.'\\+]{1,70})|\"([a-zA-Z0-9_\\-\\.'\\+\\s\\" + + "(\\),/:=\\?]{1,69}[a-zA-Z0-9_\\-\\.'\\+\\(\\),/:=\\?])\""; + private static final Pattern PATTERN_LAST_CRLF = Pattern.compile("(.*)(\r\n){1}( *)", Pattern.DOTALL); + private static final Pattern PATTERN_HEADER_LINE = Pattern.compile("([a-zA-Z\\-]+):\\s?(.*)\\s*"); + private static final String REG_EX_APPLICATION_HTTP = "application/http"; + + public static final Pattern PATTERN_MULTIPART_BOUNDARY = Pattern.compile("multipart/mixed(.*)", + Pattern.CASE_INSENSITIVE); + public static final Pattern PATTERN_CONTENT_TYPE_APPLICATION_HTTP = Pattern.compile(REG_EX_APPLICATION_HTTP, + Pattern.CASE_INSENSITIVE); + public static final String BINARY_ENCODING = "binary"; + public static final String HTTP_CONTENT_ID = "Content-Id"; + public static final String HTTP_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding"; + + public static final String HTTP_EXPECT = "Expect"; + public static final String HTTP_FROM = "From"; + public static final String HTTP_MAX_FORWARDS = "Max-Forwards"; + public static final String HTTP_RANGE = "Range"; + public static final String HTTP_TE = "TE"; + + public static String getBoundary(final String contentType, final int line) throws BatchException { + if (contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/mixed")) { + final String[] parameter = contentType.split(";"); + + for (final String pair : parameter) { + + final String[] attrValue = pair.split("="); + if (attrValue.length == 2 && "boundary".equals(attrValue[0].trim().toLowerCase(Locale.ENGLISH))) { + if (attrValue[1].matches(REG_EX_BOUNDARY)) { + return trimQuota(attrValue[1].trim()); + } else { + throw new BatchException("Invalid boundary format", BatchException.MessageKeys.INVALID_BOUNDARY, "" + line); + } + } + + } + } + throw new BatchException("Content type is not multipart mixed", + BatchException.MessageKeys.INVALID_CONTENT_TYPE, HttpContentType.MULTIPART_MIXED); + } + + public static String removeEndingSlash(String content) { + content = content.trim(); + int lastSlashIndex = content.lastIndexOf('/'); + + return (lastSlashIndex == content.length() - 1) ? content.substring(0, content.length() - 1) : content; + } + + private static String trimQuota(String boundary) { + if (boundary.matches("\".*\"")) { + boundary = boundary.replace("\"", ""); + } + + return boundary; + } + + public static List> splitMessageByBoundary(final List message, final String boundary) + throws BatchException { + final List> messageParts = new LinkedList>(); + List currentPart = new ArrayList(); + boolean isEndReached = false; + + final String quotedBoundary = Pattern.quote(boundary); + final Pattern boundaryDelimiterPattern = Pattern.compile("--" + quotedBoundary + "--[\\s ]*"); + final Pattern boundaryPattern = Pattern.compile("--" + quotedBoundary + "[\\s ]*"); + + for (Line currentLine : message) { + if (boundaryDelimiterPattern.matcher(currentLine.toString()).matches()) { + removeEndingCRLFFromList(currentPart); + messageParts.add(currentPart); + isEndReached = true; + } else if (boundaryPattern.matcher(currentLine.toString()).matches()) { + removeEndingCRLFFromList(currentPart); + messageParts.add(currentPart); + currentPart = new LinkedList(); + } else { + currentPart.add(currentLine); + } + + if (isEndReached) { + break; + } + } + + final int lineNumer = (message.size() > 0) ? message.get(0).getLineNumber() : 0; + // Remove preamble + if (messageParts.size() > 0) { + messageParts.remove(0); + } else { + throw new BatchException("Missing boundary delimiter", BatchException.MessageKeys.MISSING_BOUNDARY_DELIMITER, "" + + lineNumer); + } + + if (!isEndReached) { + throw new BatchException("Missing close boundary delimiter", BatchException.MessageKeys.MISSING_CLOSE_DELIMITER, + "" + lineNumer); + } + + if (messageParts.size() == 0) { + throw new BatchException("No boundary delimiter found", + BatchException.MessageKeys.MISSING_BOUNDARY_DELIMITER, boundary, "" + lineNumer); + } + + return messageParts; + } + + private static void removeEndingCRLFFromList(final List list) { + if (list.size() > 0) { + Line lastLine = list.remove(list.size() - 1); + list.add(removeEndingCRLF(lastLine)); + } + } + + public static Line removeEndingCRLF(final Line line) { + Pattern pattern = PATTERN_LAST_CRLF; + Matcher matcher = pattern.matcher(line.toString()); + + if (matcher.matches()) { + return new Line(matcher.group(1), line.getLineNumber()); + } else { + return line; + } + } + + public static Header consumeHeaders(final List remainingMessage) { + final int headerLineNumber = remainingMessage.size() != 0 ? remainingMessage.get(0).getLineNumber() : 0; + final Header headers = new Header(headerLineNumber); + final Iterator iter = remainingMessage.iterator(); + Line currentLine; + boolean isHeader = true; + + while (iter.hasNext() && isHeader) { + currentLine = iter.next(); + final Matcher headerMatcher = PATTERN_HEADER_LINE.matcher(currentLine.toString()); + + if (headerMatcher.matches() && headerMatcher.groupCount() == 2) { + iter.remove(); + + String headerName = headerMatcher.group(1).trim(); + String headerValue = headerMatcher.group(2).trim(); + + headers.addHeader(headerName, Header.splitValuesByComma(headerValue), currentLine.getLineNumber()); + } else { + isHeader = false; + } + } + + return headers; + } + + public static void consumeBlankLine(final List remainingMessage, final boolean isStrict) throws BatchException { + if (remainingMessage.size() > 0 && remainingMessage.get(0).toString().matches("\\s*\r\n\\s*")) { + remainingMessage.remove(0); + } else { + if (isStrict) { + final int lineNumber = (remainingMessage.size() > 0) ? remainingMessage.get(0).getLineNumber() : 0; + throw new BatchException("Missing blank line", BatchException.MessageKeys.MISSING_BLANK_LINE, "[None]", "" + + lineNumber); + } + } + } + + public static InputStream convertLineListToInputStream(List messageList) { + final String message = lineListToString(messageList); + + return new ByteArrayInputStream(message.getBytes()); + } + + private static String lineListToString(List messageList) { + final StringBuilder builder = new StringBuilder(); + + for (Line currentLine : messageList) { + builder.append(currentLine.toString()); + } + + return builder.toString(); + } + + public static String trimLineListToLength(final List list, final int length) { + final String message = lineListToString(list); + final int lastIndex = Math.min(length, message.length()); + + return (lastIndex > 0) ? message.substring(0, lastIndex) : ""; + } + + public static InputStream convertLineListToInputStream(List list, int length) { + final String message = trimLineListToLength(list, length); + + return new ByteArrayInputStream(message.getBytes()); + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchPart.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchPart.java new file mode 100644 index 000000000..104c152db --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchPart.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +public interface BatchPart { + public Header getHeaders(); + + public boolean isStrict(); +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchQueryOperation.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchQueryOperation.java new file mode 100644 index 000000000..5ff7faf96 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchQueryOperation.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import java.util.List; + +import org.apache.olingo.server.core.batch.BatchException; +import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings.Line; + +public class BatchQueryOperation implements BatchPart { + + protected final boolean isStrict; + protected Line httpStatusLine; + protected Header headers; + protected List body; + protected int bodySize; + protected List message; + + public BatchQueryOperation(final List message, final boolean isStrict) { + this.isStrict = isStrict; + this.message = message; + } + + public BatchQueryOperation parse() throws BatchException { + httpStatusLine = consumeHttpStatusLine(message); + headers = BatchParserCommon.consumeHeaders(message); + BatchParserCommon.consumeBlankLine(message, isStrict); + body = message; + + return this; + } + + protected Line consumeHttpStatusLine(final List message) throws BatchException { + if (message.size() > 0 && !message.get(0).toString().trim().equals("")) { + final Line method = message.get(0); + message.remove(0); + + return method; + } else { + final int line = (message.size() > 0) ? message.get(0).getLineNumber() : 0; + throw new BatchException("Missing http request line", BatchException.MessageKeys.INVALID_STATUS_LINE, "" + line); + } + } + + public Line getHttpStatusLine() { + return httpStatusLine; + } + + public List getBody() { + return body; + } + + public int getBodySize() { + return bodySize; + } + + @Override + public Header getHeaders() { + return headers; + } + + @Override + public boolean isStrict() { + return isStrict; + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchRequestPartImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchRequestPartImpl.java new file mode 100644 index 000000000..6c80216c4 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BatchRequestPartImpl.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.olingo.server.api.ODataRequest; +import org.apache.olingo.server.api.batch.BatchRequestPart; + +public class BatchRequestPartImpl implements BatchRequestPart { + + private List requests = new ArrayList(); + private boolean isChangeSet; + + public BatchRequestPartImpl(final boolean isChangeSet, final List requests) { + this.isChangeSet = isChangeSet; + this.requests = requests; + } + + @Override + public boolean isChangeSet() { + return isChangeSet; + } + + @Override + public List getRequests() { + return Collections.unmodifiableList(requests); + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BufferedReaderIncludingLineEndings.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BufferedReaderIncludingLineEndings.java new file mode 100644 index 000000000..aebf13b42 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/BufferedReaderIncludingLineEndings.java @@ -0,0 +1,286 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +public class BufferedReaderIncludingLineEndings extends Reader { + private static final char CR = '\r'; + private static final char LF = '\n'; + private static final int EOF = -1; + private static final int BUFFER_SIZE = 8192; + private Reader reader; + private char[] buffer; + private int offset = 0; + private int limit = 0; + + public BufferedReaderIncludingLineEndings(final Reader reader) { + this(reader, BUFFER_SIZE); + } + + public BufferedReaderIncludingLineEndings(final Reader reader, final int bufferSize) { + if (bufferSize <= 0) { + throw new IllegalArgumentException("Buffer size must be greater than zero."); + } + + this.reader = reader; + buffer = new char[bufferSize]; + } + + @Override + public int read(final char[] charBuffer, final int bufferOffset, final int length) throws IOException { + if ((bufferOffset + length) > charBuffer.length) { + throw new IndexOutOfBoundsException("Buffer is too small"); + } + + if (length < 0 || bufferOffset < 0) { + throw new IndexOutOfBoundsException("Offset and length must be grater than zero"); + } + + // Check if buffer is filled. Return if EOF is reached + // Is buffer refill required + if (limit == offset || isEOF()) { + fillBuffer(); + + if (isEOF()) { + return EOF; + } + } + + int bytesRead = 0; + int bytesToRead = length; + int currentOutputOffset = bufferOffset; + + while (bytesToRead != 0) { + // Is buffer refill required? + if (limit == offset) { + fillBuffer(); + + if (isEOF()) { + bytesToRead = 0; + } + } + + if (bytesToRead > 0) { + int readByte = Math.min(limit - offset, bytesToRead); + bytesRead += readByte; + bytesToRead -= readByte; + + for (int i = 0; i < readByte; i++) { + charBuffer[currentOutputOffset++] = buffer[offset++]; + } + } + } + + return bytesRead; + } + + public List toList() throws IOException { + final List result = new ArrayList(); + String currentLine; + + while ((currentLine = readLine()) != null) { + result.add(currentLine); + } + + return result; + } + + public List toLineList() throws IOException { + final List result = new ArrayList(); + String currentLine; + int counter = 1; + + while ((currentLine = readLine()) != null) { + result.add(new Line(currentLine, counter++)); + } + + return result; + } + + public String readLine() throws IOException { + if (limit == EOF) { + return null; + } + + final StringBuilder stringBuffer = new StringBuilder(); + boolean foundLineEnd = false; // EOF will be considered as line ending + + while (!foundLineEnd) { + // Is buffer refill required? + if (limit == offset) { + if (fillBuffer() == EOF) { + foundLineEnd = true; + } + } + + if (!foundLineEnd) { + char currentChar = buffer[offset++]; + stringBuffer.append(currentChar); + + if (currentChar == LF) { + foundLineEnd = true; + } else if (currentChar == CR) { + foundLineEnd = true; + + // Check next char. Consume \n if available + // Is buffer refill required? + if (limit == offset) { + fillBuffer(); + } + + // Check if there is at least one character + if (limit != EOF && buffer[offset] == LF) { + stringBuffer.append(LF); + offset++; + } + } + } + } + + return (stringBuffer.length() == 0) ? null : stringBuffer.toString(); + } + + @Override + public void close() throws IOException { + reader.close(); + } + + @Override + public boolean ready() throws IOException { + // Not EOF and buffer refill is not required + return !isEOF() && !(limit == offset); + } + + @Override + public void reset() throws IOException { + throw new IOException("Reset is not supported"); + } + + @Override + public void mark(final int readAheadLimit) throws IOException { + throw new IOException("Mark is not supported"); + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public long skip(final long n) throws IOException { + if (n == 0) { + return 0; + } else if (n < 0) { + throw new IllegalArgumentException("skip value is negative"); + } else { + long charactersToSkip = n; + long charactersSkiped = 0; + + while (charactersToSkip != 0) { + // Is buffer refill required? + if (limit == offset) { + fillBuffer(); + + if (isEOF()) { + charactersToSkip = 0; + } + } + + // Check if more characters are available + if (!isEOF()) { + int skipChars = (int) Math.min(limit - offset, charactersToSkip); + + charactersSkiped += skipChars; + charactersToSkip -= skipChars; + offset += skipChars; + } + } + + return charactersSkiped; + } + } + + private boolean isEOF() { + return limit == EOF; + } + + private int fillBuffer() throws IOException { + limit = reader.read(buffer, 0, buffer.length); + offset = 0; + + return limit; + } + + public static class Line { + private final int lineNumber; + private final String content; + + public Line(final String content, final int lineNumber) { + this.content = content; + this.lineNumber = lineNumber; + } + + public int getLineNumber() { + return lineNumber; + } + + @Override + public String toString() { + return content; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((content == null) ? 0 : content.hashCode()); + result = prime * result + lineNumber; + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Line other = (Line) obj; + if (content == null) { + if (other.content != null) { + return false; + } + } else if (!content.equals(other.content)) { + return false; + } + if (lineNumber != other.lineNumber) { + return false; + } + return true; + } + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/Header.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/Header.java new file mode 100644 index 000000000..2ac009d23 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/Header.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Pattern; + +public class Header implements Iterable { + private final Map headers = new HashMap(); + private int lineNumber; + + public Header(final int lineNumer) { + lineNumber = lineNumer; + } + + public void addHeader(final String name, final String value, final int lineNumber) { + final HeaderField headerField = getHeaderFieldOrDefault(name, lineNumber); + final List headerValues = headerField.getValues(); + + if (!headerValues.contains(value)) { + headerValues.add(value); + } + } + + public void addHeader(final String name, final List values, final int lineNumber) { + final HeaderField headerField = getHeaderFieldOrDefault(name, lineNumber); + final List headerValues = headerField.getValues(); + + for (final String value : values) { + if (!headerValues.contains(value)) { + headerValues.add(value); + } + } + } + + public void replaceHeaderField(final HeaderField headerField) { + headers.put(headerField.getFieldName().toLowerCase(Locale.ENGLISH), headerField); + } + + public boolean exists(final String name) { + final HeaderField field = headers.get(name.toLowerCase(Locale.ENGLISH)); + + return field != null && field.getValues().size() != 0; + } + + public boolean isHeaderMatching(final String name, final Pattern pattern) { + if (getHeaders(name).size() != 1) { + return false; + } else { + return pattern.matcher(getHeaders(name).get(0)).matches(); + } + } + + public void removeHeader(final String name) { + headers.remove(name.toLowerCase(Locale.ENGLISH)); + } + + public String getHeader(final String name) { + final HeaderField headerField = getHeaderField(name); + + return (headerField == null) ? null : headerField.getValue(); + } + + public String getHeaderNotNull(final String name) { + final HeaderField headerField = getHeaderField(name); + + return (headerField == null) ? "" : headerField.getValueNotNull(); + } + + public List getHeaders(final String name) { + final HeaderField headerField = getHeaderField(name); + + return (headerField == null) ? new ArrayList() : headerField.getValues(); + } + + public HeaderField getHeaderField(final String name) { + return headers.get(name.toLowerCase(Locale.ENGLISH)); + } + + public int getLineNumber() { + return lineNumber; + } + + public Map toSingleMap() { + final Map singleMap = new HashMap(); + + for (final String key : headers.keySet()) { + HeaderField field = headers.get(key); + singleMap.put(field.getFieldName(), getHeader(key)); + } + + return singleMap; + } + + public Map> toMultiMap() { + final Map> singleMap = new HashMap>(); + + for (final String key : headers.keySet()) { + HeaderField field = headers.get(key); + singleMap.put(field.getFieldName(), field.getValues()); + } + + return singleMap; + } + + private HeaderField getHeaderFieldOrDefault(final String name, final int lineNumber) { + HeaderField headerField = headers.get(name.toLowerCase(Locale.ENGLISH)); + + if (headerField == null) { + headerField = new HeaderField(name, lineNumber); + headers.put(name.toLowerCase(Locale.ENGLISH), headerField); + } + + return headerField; + } + + @Override + public Header clone() { + final Header newInstance = new Header(lineNumber); + + for (final String key : headers.keySet()) { + newInstance.headers.put(key, headers.get(key).clone()); + } + + return newInstance; + } + + @Override + public Iterator iterator() { + return new Iterator() { + Iterator keyIterator = headers.keySet().iterator(); + + @Override + public boolean hasNext() { + return keyIterator.hasNext(); + } + + @Override + public HeaderField next() { + return headers.get(keyIterator.next()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + public static List splitValuesByComma(final String headerValue) { + final List singleValues = new ArrayList(); + + String[] parts = headerValue.split(","); + for (final String value : parts) { + singleValues.add(value.trim()); + } + + return singleValues; + } +} \ No newline at end of file diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/HeaderField.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/HeaderField.java new file mode 100644 index 000000000..4cad817ab --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/parser/HeaderField.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import java.util.ArrayList; +import java.util.List; + + public class HeaderField implements Cloneable { + private final String fieldName; + private final List values; + private final int lineNumber; + + public HeaderField(final String fieldName, final int lineNumber) { + this(fieldName, new ArrayList(), lineNumber); + } + + public HeaderField(final String fieldName, final List values, final int lineNumber) { + this.fieldName = fieldName; + this.values = values; + this.lineNumber = lineNumber; + } + + public String getFieldName() { + return fieldName; + } + + public List getValues() { + return values; + } + + public String getValue() { + final StringBuilder result = new StringBuilder(); + + for (final String value : values) { + result.append(value); + result.append(", "); + } + + if (result.length() > 0) { + result.delete(result.length() - 2, result.length()); + } + + return result.toString(); + } + + public String getValueNotNull() { + final String value = getValue(); + + return (value == null) ? "" : value; + } + + @Override + public HeaderField clone() { + List newValues = new ArrayList(); + newValues.addAll(values); + + return new HeaderField(fieldName, newValues, lineNumber); + } + + public int getLineNumber() { + return lineNumber; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((fieldName == null) ? 0 : fieldName.hashCode()); + result = prime * result + lineNumber; + result = prime * result + ((values == null) ? 0 : values.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + HeaderField other = (HeaderField) obj; + if (fieldName == null) { + if (other.fieldName != null) { + return false; + } + } else if (!fieldName.equals(other.fieldName)) { + return false; + } + if (lineNumber != other.lineNumber) { + return false; + } + if (values == null) { + if (other.values != null) { + return false; + } + } else if (!values.equals(other.values)) { + return false; + } + return true; + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchRequestTransformator.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchRequestTransformator.java new file mode 100644 index 000000000..02c811897 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchRequestTransformator.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.transformator; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.api.http.HttpMethod; +import org.apache.olingo.server.api.ODataRequest; +import org.apache.olingo.server.api.batch.BatchParserResult; +import org.apache.olingo.server.core.batch.BatchException; +import org.apache.olingo.server.core.batch.BatchException.MessageKeys; +import org.apache.olingo.server.core.batch.parser.BatchBodyPart; +import org.apache.olingo.server.core.batch.parser.BatchChangeSetPart; +import org.apache.olingo.server.core.batch.parser.BatchParserCommon; +import org.apache.olingo.server.core.batch.parser.BatchPart; +import org.apache.olingo.server.core.batch.parser.BatchQueryOperation; +import org.apache.olingo.server.core.batch.parser.BatchRequestPartImpl; +import org.apache.olingo.server.core.batch.parser.Header; +import org.apache.olingo.server.core.batch.parser.HeaderField; +import org.apache.olingo.server.core.batch.transformator.BatchTransformatorCommon.HttpRequestStatusLine; + +public class BatchRequestTransformator implements BatchTransformator { + private final String baseUri; + private final String rawServiceResolutionUri; + + public BatchRequestTransformator(final String baseUri, final String serviceResolutionUri) { + this.baseUri = baseUri; + this.rawServiceResolutionUri = serviceResolutionUri; + } + + @Override + public List transform(final BatchBodyPart bodyPart) throws BatchException { + final List requests = new LinkedList(); + final List resultList = new ArrayList(); + + validateBodyPartHeader(bodyPart); + + for (BatchQueryOperation queryOperation : bodyPart.getRequests()) { + requests.add(processQueryOperation(bodyPart, baseUri, queryOperation)); + } + + resultList.add(new BatchRequestPartImpl(bodyPart.isChangeSet(), requests)); + return resultList; + } + + private ODataRequest + processQueryOperation(BatchBodyPart bodyPart, String baseUri, BatchQueryOperation queryOperation) + throws BatchException { + if (bodyPart.isChangeSet()) { + BatchQueryOperation encapsulatedQueryOperation = ((BatchChangeSetPart) queryOperation).getRequest(); + handleContentId(queryOperation, encapsulatedQueryOperation); + validateHeader(queryOperation, true); + + return createRequest(encapsulatedQueryOperation, baseUri, bodyPart.isChangeSet()); + } else { + return createRequest(queryOperation, baseUri, bodyPart.isChangeSet()); + } + } + + private void handleContentId(BatchQueryOperation changeRequestPart, BatchQueryOperation request) + throws BatchException { + final HeaderField contentIdChangeRequestPart = getContentId(changeRequestPart); + final HeaderField contentIdRequest = getContentId(request); + + if (contentIdChangeRequestPart == null && contentIdRequest == null) { + throw new BatchException("Missing content type", MessageKeys.MISSING_CONTENT_ID, changeRequestPart.getHeaders() + .getLineNumber()); + } else if (contentIdChangeRequestPart != null) { + request.getHeaders().replaceHeaderField(contentIdChangeRequestPart); + } + } + + private HeaderField getContentId(final BatchQueryOperation queryOperation) throws BatchException { + final HeaderField contentTypeHeader = queryOperation.getHeaders().getHeaderField(BatchParserCommon.HTTP_CONTENT_ID); + + if (contentTypeHeader != null) { + if (contentTypeHeader.getValues().size() == 1) { + return contentTypeHeader; + } else { + throw new BatchException("Invalid header", MessageKeys.INVALID_HEADER, contentTypeHeader.getLineNumber()); + } + } + + return null; + } + + private ODataRequest createRequest(BatchQueryOperation operation, String baseUri, boolean isChangeSet) + throws BatchException { + final HttpRequestStatusLine statusLine = + new HttpRequestStatusLine(operation.getHttpStatusLine(), baseUri, rawServiceResolutionUri, operation + .getHeaders()); + statusLine.validateHttpMethod(isChangeSet); + + validateBody(statusLine, operation); + InputStream bodyStrean = getBodyStream(operation, statusLine); + + validateForbiddenHeader(operation); + + final ODataRequest request = new ODataRequest(); + request.setBody(bodyStrean); + request.setMethod(statusLine.getMethod()); + request.setRawBaseUri(statusLine.getRawBaseUri()); + request.setRawODataPath(statusLine.getRawODataPath()); + request.setRawQueryPath(statusLine.getRawQueryPath()); + request.setRawRequestUri(statusLine.getRawRequestUri()); + request.setRawServiceResolutionUri(statusLine.getRawServiceResolutionUri()); + + for (final HeaderField field : operation.getHeaders()) { + request.addHeader(field.getFieldName(), field.getValues()); + } + + return request; + } + + private void validateForbiddenHeader(BatchQueryOperation operation) throws BatchException { + final Header header = operation.getHeaders(); + + if (header.exists(HttpHeader.AUTHORIZATION) || header.exists(BatchParserCommon.HTTP_EXPECT) + || header.exists(BatchParserCommon.HTTP_FROM) || header.exists(BatchParserCommon.HTTP_MAX_FORWARDS) + || header.exists(BatchParserCommon.HTTP_RANGE) || header.exists(BatchParserCommon.HTTP_TE)) { + throw new BatchException("Forbidden header", MessageKeys.FORBIDDEN_HEADER, header.getLineNumber()); + } + } + + private InputStream getBodyStream(BatchQueryOperation operation, HttpRequestStatusLine statusLine) + throws BatchException { + if (statusLine.getMethod().equals(HttpMethod.GET)) { + return new ByteArrayInputStream(new byte[0]); + } else { + int contentLength = BatchTransformatorCommon.getContentLength(operation.getHeaders()); + + if (contentLength == -1) { + return BatchParserCommon.convertLineListToInputStream(operation.getBody()); + } else { + return BatchParserCommon.convertLineListToInputStream(operation.getBody(), contentLength); + } + } + } + + private void validateBody(HttpRequestStatusLine statusLine, BatchQueryOperation operation) throws BatchException { + if (statusLine.getMethod().equals(HttpMethod.GET) && isUnvalidGetRequestBody(operation)) { + throw new BatchException("Invalid request line", MessageKeys.INVALID_CONTENT, statusLine.getLineNumber()); + } + } + + private boolean isUnvalidGetRequestBody(final BatchQueryOperation operation) { + return (operation.getBody().size() > 1) + || (operation.getBody().size() == 1 && !"".equals(operation.getBody().get(0).toString().trim())); + } + + private void validateHeader(BatchPart bodyPart, boolean isChangeSet) throws BatchException { + final Header headers = bodyPart.getHeaders(); + + BatchTransformatorCommon.validateContentType(headers, BatchParserCommon.PATTERN_CONTENT_TYPE_APPLICATION_HTTP); + if (isChangeSet) { + BatchTransformatorCommon.validateContentTransferEncoding(headers); + } + } + + private void validateBodyPartHeader(BatchBodyPart bodyPart) throws BatchException { + final Header header = bodyPart.getHeaders(); + + if (bodyPart.isChangeSet()) { + BatchTransformatorCommon.validateContentType(header, BatchParserCommon.PATTERN_MULTIPART_BOUNDARY); + } else { + BatchTransformatorCommon.validateContentTransferEncoding(header); + BatchTransformatorCommon.validateContentType(header, BatchParserCommon.PATTERN_CONTENT_TYPE_APPLICATION_HTTP); + } + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformator.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformator.java new file mode 100644 index 000000000..becb6c765 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformator.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.transformator; + +import java.util.List; + +import org.apache.olingo.server.api.batch.BatchParserResult; +import org.apache.olingo.server.core.batch.BatchException; +import org.apache.olingo.server.core.batch.parser.BatchBodyPart; + +public interface BatchTransformator { + public List transform(BatchBodyPart bodyPart) throws BatchException; +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformatorCommon.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformatorCommon.java new file mode 100644 index 000000000..a351cca7c --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/transformator/BatchTransformatorCommon.java @@ -0,0 +1,250 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.transformator; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.olingo.commons.api.http.HttpContentType; +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.api.http.HttpMethod; +import org.apache.olingo.server.core.batch.BatchException; +import org.apache.olingo.server.core.batch.BatchException.MessageKeys; +import org.apache.olingo.server.core.batch.parser.BatchParserCommon; +import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings.Line; +import org.apache.olingo.server.core.batch.parser.Header; +import org.apache.olingo.server.core.batch.parser.HeaderField; + +public class BatchTransformatorCommon { + + public static void validateContentType(final Header headers, final Pattern pattern) throws BatchException { + List contentTypes = headers.getHeaders(HttpHeader.CONTENT_TYPE); + + if (contentTypes.size() == 0) { + throw new BatchException("Missing content type", MessageKeys.MISSING_CONTENT_TYPE, headers.getLineNumber()); + } + if (!headers.isHeaderMatching(HttpHeader.CONTENT_TYPE, pattern)) { + + throw new BatchException("Invalid content type", MessageKeys.INVALID_CONTENT_TYPE, + HttpContentType.MULTIPART_MIXED + " or " + HttpContentType.APPLICATION_HTTP); + } + } + + public static void validateContentTransferEncoding(Header headers) throws BatchException { + final HeaderField contentTransferField = headers.getHeaderField(BatchParserCommon.HTTP_CONTENT_TRANSFER_ENCODING); + + if (contentTransferField != null) { + final List contentTransferValues = contentTransferField.getValues(); + if (contentTransferValues.size() == 1) { + String encoding = contentTransferValues.get(0); + + if (!BatchParserCommon.BINARY_ENCODING.equalsIgnoreCase(encoding)) { + throw new BatchException("Invalid content transfer encoding", MessageKeys.INVALID_CONTENT_TRANSFER_ENCODING, + headers.getLineNumber()); + } + } else { + throw new BatchException("Invalid header", MessageKeys.INVALID_HEADER, headers.getLineNumber()); + } + } else { + throw new BatchException("Missing mandatory content transfer encoding", + MessageKeys.MISSING_CONTENT_TRANSFER_ENCODING, + headers.getLineNumber()); + } + } + + public static int getContentLength(Header headers) throws BatchException { + final HeaderField contentLengthField = headers.getHeaderField(HttpHeader.CONTENT_LENGTH); + + if (contentLengthField != null && contentLengthField.getValues().size() == 1) { + final List contentLengthValues = contentLengthField.getValues(); + + try { + int contentLength = Integer.parseInt(contentLengthValues.get(0)); + + if (contentLength < 0) { + throw new BatchException("Invalid content length", MessageKeys.INVALID_CONTENT_LENGTH, contentLengthField + .getLineNumber()); + } + + return contentLength; + } catch (NumberFormatException e) { + throw new BatchException("Invalid header", MessageKeys.INVALID_HEADER, contentLengthField.getLineNumber()); + } + } + + return -1; + } + + public static class HttpRequestStatusLine { + private static final Pattern PATTERN_RELATIVE_URI = Pattern.compile("([^/][^?]*)(?:\\?(.*))?"); + private static final Pattern PATTERN_ABSOLUTE_URI_WITH_HOST = Pattern.compile("(/[^?]*)(?:\\?(.*))?"); + private static final Pattern PATTERN_ABSOLUTE_URI = Pattern.compile("(http[s]?://[^?]*)(?:\\?(.*))?"); + + private static final Set HTTP_BATCH_METHODS = new HashSet(Arrays.asList(new String[] { "GET" })); + private static final Set HTTP_CHANGE_SET_METHODS = new HashSet(Arrays.asList(new String[] { "POST", + "PUT", "DELETE", "MERGE", "PATCH" })); + private static final String HTTP_VERSION = "HTTP/1.1"; + + final private Line statusLine; + final String requestBaseUri; + + private HttpMethod method; + private String httpVersion; + private String rawServiceResolutionUri; + private String rawQueryPath; + private String rawODataPath; + private String rawBaseUri; + private Header header; + private String rawRequestUri; + + public HttpRequestStatusLine(final Line httpStatusLine, final String baseUri, final String serviceResolutionUri, + final Header requestHeader) + throws BatchException { + statusLine = httpStatusLine; + requestBaseUri = baseUri; + header = requestHeader; + rawServiceResolutionUri = serviceResolutionUri; + + parse(); + } + + private void parse() throws BatchException { + final String[] parts = statusLine.toString().split(" "); + + if (parts.length == 3) { + method = parseMethod(parts[0]); + parseRawPaths(parts[1]); + httpVersion = parseHttpVersion(parts[2]); + } else { + throw new BatchException("Invalid status line", MessageKeys.INVALID_STATUS_LINE, statusLine.getLineNumber()); + } + } + + private HttpMethod parseMethod(final String method) throws BatchException { + try { + return HttpMethod.valueOf(method.trim()); + } catch (IllegalArgumentException e) { + throw new BatchException("Illegal http method", MessageKeys.INVALID_METHOD, statusLine.getLineNumber()); + } + } + + private void parseRawPaths(final String rawUrl) throws BatchException { + final Matcher absoluteUriMatcher = PATTERN_ABSOLUTE_URI.matcher(rawUrl); + final Matcher absoluteUriWtithHostMatcher = PATTERN_ABSOLUTE_URI_WITH_HOST.matcher(rawUrl); + final Matcher relativeUriMatcher = PATTERN_RELATIVE_URI.matcher(rawUrl); + + if (absoluteUriMatcher.matches()) { + buildUri(absoluteUriMatcher.group(1), absoluteUriMatcher.group(2)); + + } else if (absoluteUriWtithHostMatcher.matches()) { + final List hostHeader = header.getHeaders(HttpHeader.HOST); + if (hostHeader.size() == 1) { + buildUri(hostHeader.get(0) + absoluteUriWtithHostMatcher.group(1), absoluteUriWtithHostMatcher.group(2)); + } else { + throw new BatchException("Exactly one host header is required", MessageKeys.MISSING_MANDATORY_HEADER, + statusLine.getLineNumber()); + } + + } else if (relativeUriMatcher.matches()) { + buildUri(requestBaseUri + "/" + relativeUriMatcher.group(1), relativeUriMatcher.group(2)); + + } else { + throw new BatchException("Invalid uri", MessageKeys.INVALID_URI, statusLine.getLineNumber()); + } + } + + private void buildUri(final String resourceUri, final String queryOptions) throws BatchException { + if(!resourceUri.startsWith(requestBaseUri)) { + throw new BatchException("Host do not match", MessageKeys.INVALID_URI, statusLine.getLineNumber()); + } + + final int oDataPathIndex = resourceUri.indexOf(requestBaseUri); + + rawBaseUri = requestBaseUri; + rawODataPath = resourceUri.substring(oDataPathIndex + requestBaseUri.length()); + rawServiceResolutionUri = ""; + rawRequestUri = requestBaseUri + rawODataPath; + + if (queryOptions != null) { + rawRequestUri += "?" + queryOptions; + rawQueryPath = queryOptions; + } else { + rawQueryPath = ""; + } + } + + private String parseHttpVersion(final String httpVersion) throws BatchException { + if (!HTTP_VERSION.equals(httpVersion.trim())) { + throw new BatchException("Invalid http version", MessageKeys.INVALID_HTTP_VERSION, statusLine.getLineNumber()); + } else { + return HTTP_VERSION; + } + } + + public void validateHttpMethod(boolean isChangeSet) throws BatchException { + Set validMethods = (isChangeSet) ? HTTP_CHANGE_SET_METHODS : HTTP_BATCH_METHODS; + + if (!validMethods.contains(getMethod().toString())) { + if (isChangeSet) { + throw new BatchException("Invalid change set method", MessageKeys.INVALID_CHANGESET_METHOD, statusLine + .getLineNumber()); + } else { + throw new BatchException("Invalid query operation method", MessageKeys.INVALID_QUERY_OPERATION_METHOD, + statusLine.getLineNumber()); + } + } + } + + public HttpMethod getMethod() { + return method; + } + + public String getHttpVersion() { + return httpVersion; + } + + public int getLineNumber() { + return statusLine.getLineNumber(); + } + + public String getRawBaseUri() { + return rawBaseUri; + } + + public String getRawODataPath() { + return rawODataPath; + } + + public String getRawQueryPath() { + return rawQueryPath; + } + + public String getRawRequestUri() { + return rawRequestUri; + } + + public String getRawServiceResolutionUri() { + return rawServiceResolutionUri; + } + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/writer/BatchResponseWriter.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/writer/BatchResponseWriter.java new file mode 100644 index 000000000..a0637477e --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/writer/BatchResponseWriter.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.writer; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.olingo.commons.api.http.HttpContentType; +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.batch.ODataResponsePart; +import org.apache.olingo.server.core.batch.BatchException; +import org.apache.olingo.server.core.batch.BatchException.MessageKeys; +import org.apache.olingo.server.core.batch.parser.BatchParserCommon; +import org.apache.olingo.server.core.serializer.utils.CircleStreamBuffer; + +public class BatchResponseWriter { + private static final int BUFFER_SIZE = 4096; + private static final String DOUBLE_DASH = "--"; + private static final String COLON = ":"; + private static final String SP = " "; + private static final String CRLF = "\r\n"; + + public void toODataResponse(final List batchResponse, final ODataResponse response) + throws IOException, BatchException { + final String boundary = generateBoundary("batch"); + + setStatusCode(response); + ResponseWriter writer = createBody(batchResponse, boundary); + + response.setContent(writer.toInputStream()); + setHttpHeader(response, writer, boundary); + } + + private ResponseWriter createBody(final List batchResponses, final String boundary) + throws IOException, BatchException { + final ResponseWriter writer = new ResponseWriter(); + + for (final ODataResponsePart part : batchResponses) { + writer.append(getDashBoundary(boundary)); + + if (part.isChangeSet()) { + appendChangeSet(part, writer); + } else { + appendBodyPart(part.getResponses().get(0), writer, false); + } + } + writer.append(getCloseDelimiter(boundary)); + + return writer; + } + + private void appendChangeSet(ODataResponsePart part, ResponseWriter writer) throws IOException, BatchException { + final String changeSetBoundary = generateBoundary("changeset"); + + appendChangeSetHeader(writer, changeSetBoundary); + writer.append(CRLF); + + for (final ODataResponse response : part.getResponses()) { + writer.append(getDashBoundary(changeSetBoundary)); + appendBodyPart(response, writer, true); + } + + writer.append(getCloseDelimiter(changeSetBoundary)); + writer.append(CRLF); + } + + private void appendBodyPart(ODataResponse response, ResponseWriter writer, boolean isChangeSet) throws IOException, + BatchException { + byte[] body = getBody(response); + + appendBodyPartHeader(response, writer, isChangeSet); + writer.append(CRLF); + + appendStatusLine(response, writer); + appendResponseHeader(response, body.length, writer); + writer.append(CRLF); + + writer.append(body); + writer.append(CRLF); + } + + private byte[] getBody(final ODataResponse response) throws IOException { + final InputStream content = response.getContent(); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + if (content != null) { + byte[] buffer = new byte[BUFFER_SIZE]; + int n; + + while ((n = content.read(buffer, 0, buffer.length)) != -1) { + out.write(buffer, 0, n); + } + out.flush(); + + return out.toByteArray(); + } else { + return new byte[0]; + } + } + + private void appendChangeSetHeader(ResponseWriter writer, final String changeSetBoundary) throws IOException { + appendHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED.toString() + "; boundary=" + + changeSetBoundary, writer); + } + + private void appendHeader(String name, String value, ResponseWriter writer) throws IOException { + writer.append(name) + .append(COLON) + .append(SP) + .append(value) + .append(CRLF); + } + + private void appendStatusLine(ODataResponse response, ResponseWriter writer) throws IOException { + writer.append("HTTP/1.1") + .append(SP) + .append("" + response.getStatusCode()) + .append(SP) + .append(HttpStatusCode.fromStatusCode(response.getStatusCode()).toString()) + .append(CRLF); + } + + private void appendResponseHeader(ODataResponse response, int contentLength, ResponseWriter writer) + throws IOException { + final Map header = response.getHeaders(); + + for (final String key : header.keySet()) { + // Requests do never have content id header + if (!key.equalsIgnoreCase(BatchParserCommon.HTTP_CONTENT_ID)) { + appendHeader(key, header.get(key), writer); + } + } + + appendHeader(HttpHeader.CONTENT_LENGTH, "" + contentLength, writer); + } + + private void appendBodyPartHeader(ODataResponse response, ResponseWriter writer, boolean isChangeSet) + throws BatchException, IOException { + appendHeader(HttpHeader.CONTENT_TYPE, HttpContentType.APPLICATION_HTTP, writer); + appendHeader(BatchParserCommon.HTTP_CONTENT_TRANSFER_ENCODING, BatchParserCommon.BINARY_ENCODING, writer); + + if (isChangeSet) { + if (response.getHeaders().get(BatchParserCommon.HTTP_CONTENT_ID) != null) { + appendHeader(BatchParserCommon.HTTP_CONTENT_ID, response.getHeaders().get(BatchParserCommon.HTTP_CONTENT_ID), + writer); + } else { + throw new BatchException("Missing content id", MessageKeys.MISSING_CONTENT_ID, ""); + } + } + } + + private void setHttpHeader(ODataResponse response, ResponseWriter writer, final String boundary) { + response.setHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED.toString() + "; boundary=" + boundary); + response.setHeader(HttpHeader.CONTENT_LENGTH, "" + writer.length()); + } + + private void setStatusCode(final ODataResponse response) { + response.setStatusCode(HttpStatusCode.ACCEPTED.getStatusCode()); + } + + private String getDashBoundary(String boundary) { + return DOUBLE_DASH + boundary + CRLF; + } + + private String getCloseDelimiter(final String boundary) { + return DOUBLE_DASH + boundary + DOUBLE_DASH + CRLF; + } + + private String generateBoundary(final String value) { + return value + "_" + UUID.randomUUID().toString(); + } + + private static class ResponseWriter { + private CircleStreamBuffer buffer = new CircleStreamBuffer(); + private BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(buffer.getOutputStream())); + private int length = 0; + + public ResponseWriter append(final String content) throws IOException { + length += content.length(); + writer.write(content); + + return this; + } + + public ResponseWriter append(final byte[] content) throws IOException { + length += content.length; + writer.flush(); + buffer.getOutputStream().write(content, 0, content.length); + + return this; + } + + public int length() { + return length; + } + + public InputStream toInputStream() throws IOException { + writer.flush(); + writer.close(); + + return buffer.getInputStream(); + } + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/writer/ODataResponsePartImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/writer/ODataResponsePartImpl.java new file mode 100644 index 000000000..a9711c96f --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/batch/writer/ODataResponsePartImpl.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.writer; + +import java.util.List; + +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.batch.ODataResponsePart; + +public class ODataResponsePartImpl implements ODataResponsePart { + private final List responses; + private final boolean isChangeSet; + + public ODataResponsePartImpl(final List responses, final boolean isChangeSet) { + this.responses = responses; + this.isChangeSet = isChangeSet; + } + + @Override + public List getResponses() { + return responses; + } + + @Override + public boolean isChangeSet() { + return isChangeSet; + } +} diff --git a/lib/server-core/src/main/resources/server-core-exceptions-i18n.properties b/lib/server-core/src/main/resources/server-core-exceptions-i18n.properties index 15f4ab8f9..7e58fc658 100644 --- a/lib/server-core/src/main/resources/server-core-exceptions-i18n.properties +++ b/lib/server-core/src/main/resources/server-core-exceptions-i18n.properties @@ -94,3 +94,24 @@ SerializerException.UNSUPPORTED_PROPERTY_TYPE=The type of the property '%1$s' is SerializerException.INCONSISTENT_PROPERTY_TYPE=An inconsistency has been detected in the type definition of property '%1$s'. SerializerException.MISSING_PROPERTY=The non-nullable property '%1$s' is missing. SerializerException.WRONG_PROPERTY_VALUE=The value '%2$s' is not valid for property '%1$s'. + +BatchException.INVALID_BOUNDARY=Invalid boundary at line '%1$s'. +BatchException.INVALID_CHANGESET_METHOD=Invalid method: a ChangeSet cannot contain retrieve requests at line '%1$s'. +BatchException.INVALID_CONTENT=Retrieve requests must not contain any body content '%1$s'. +BatchException.INVALID_CONTENT_LENGTH=Invalid content length: content length have to be an integer and positive at line '%1$s'. +BatchException.INVALID_CONTENT_TRANSFER_ENCODING=The Content-Transfer-Encoding should be binary: line '%1$s'. +BatchException.INVALID_CONTENT_TYPE=Content-Type should be '%1$s'. +BatchException.INVALID_HEADER=Invalid header: '%1$s' at line '%2$s'. +BatchException.INVALID_HTTP_VERSION=Invalid HTTP version: The version have to be HTTP/1.1 at line '%1$s'. +BatchException.INVALID_METHOD=Invalid HTTP method at line '%1$s'. +BatchException.INVALID_QUERY_OPERATION_METHOD=Invalid method: a query operation can only contain retrieve requests at line '%1$s'. +BatchException.INVALID_STATUS_LINE=Invalid HTTP status line at line '%1$s'. +BatchException.INVALID_URI=Invalid URI at line '%1$s'. +BatchException.FORBIDDEN_HEADER=Forbidden header at line '%1$s'. +BatchException.MISSING_BLANK_LINE=Missing blank line at line '%1$s'. +BatchException.MISSING_BOUNDARY_DELIMITER=Missing boundary delimiter at line '%1$s'. +BatchException.MISSING_CLOSE_DELIMITER=Missing close delimiter at line '%1$s'. +BatchException.MISSING_CONTENT_ID=Missing content-id at line '%1$s'. +BatchException.MISSING_CONTENT_TRANSFER_ENCODING=Missing content transfer encoding at line '%1$s'. +BatchException.MISSING_CONTENT_TYPE=Missing content-type at line '%1$s'. +BatchException.MISSING_MANDATORY_HEADER=Missing mandatory header at line '%1$s'. \ No newline at end of file diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/BatchRequestParserTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/BatchRequestParserTest.java new file mode 100644 index 000000000..1d68307a4 --- /dev/null +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/BatchRequestParserTest.java @@ -0,0 +1,1308 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.util.List; + +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.api.http.HttpMethod; +import org.apache.olingo.server.api.ODataRequest; +import org.apache.olingo.server.api.batch.BatchRequestPart; +import org.apache.olingo.server.core.batch.BatchException.MessageKeys; +import org.apache.olingo.server.core.batch.parser.BatchParser; +import org.apache.olingo.server.core.batch.parser.BatchParserCommon; +import org.junit.Test; + +public class BatchRequestParserTest { + + private static final String SERVICE_ROOT = "http://localhost/odata"; + private static final String CONTENT_TYPE = "multipart/mixed;boundary=batch_8194-cf13-1f56"; + private static final String CRLF = "\r\n"; + private static final String MIME_HEADERS = "Content-Type: application/http" + CRLF + + "Content-Transfer-Encoding: binary" + CRLF; + private static final String GET_REQUEST = "" + + MIME_HEADERS + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF + + CRLF + + CRLF; + + @Test + public void test() throws IOException, BatchException, URISyntaxException { + final InputStream in = readFile("/batchWithPost.batch"); + final List batchRequestParts = parse(in); + + assertNotNull(batchRequestParts); + assertFalse(batchRequestParts.isEmpty()); + + for (BatchRequestPart object : batchRequestParts) { + if (!object.isChangeSet()) { + assertEquals(1, object.getRequests().size()); + ODataRequest retrieveRequest = object.getRequests().get(0); + assertEquals(HttpMethod.GET, retrieveRequest.getMethod()); + + if (retrieveRequest.getHeaders(HttpHeader.ACCEPT_LANGUAGE) != null) { + assertEquals(3, retrieveRequest.getHeaders(HttpHeader.ACCEPT_LANGUAGE).size()); + } + + assertEquals(SERVICE_ROOT, retrieveRequest.getRawBaseUri()); + assertEquals("/Employees('2')/EmployeeName", retrieveRequest.getRawODataPath()); + assertEquals("http://localhost/odata/Employees('2')/EmployeeName?$format=json", retrieveRequest + .getRawRequestUri()); + assertEquals("$format=json", retrieveRequest.getRawQueryPath()); + } else { + List requests = object.getRequests(); + for (ODataRequest request : requests) { + + assertEquals(HttpMethod.PUT, request.getMethod()); + assertEquals("100000", request.getHeader(HttpHeader.CONTENT_LENGTH)); + assertEquals("application/json;odata=verbose", request.getHeader(HttpHeader.CONTENT_TYPE)); + + List acceptHeader = request.getHeaders(HttpHeader.ACCEPT); + assertEquals(3, request.getHeaders(HttpHeader.ACCEPT).size()); + assertEquals("application/atomsvc+xml;q=0.8", acceptHeader.get(0)); + assertEquals("*/*;q=0.1", acceptHeader.get(2)); + + assertEquals("http://localhost/odata/Employees('2')/EmployeeName", request.getRawRequestUri()); + assertEquals("http://localhost/odata", request.getRawBaseUri()); + assertEquals("/Employees('2')/EmployeeName", request.getRawODataPath()); + assertEquals("", request.getRawQueryPath()); // No query parameter + } + } + } + } + + @Test + public void testImageInContent() throws IOException, BatchException, URISyntaxException { + final InputStream contentInputStream = readFile("/batchWithContent.batch"); + final String content = StringUtil.toString(contentInputStream); + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees?$filter=Age%20gt%2040 HTTP/1.1" + CRLF + + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF + + "MaxDataServiceVersion: 2.0" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + "content-type: Application/http" + CRLF + + "content-transfer-encoding: Binary" + CRLF + + "Content-ID: 1" + CRLF + + CRLF + + "POST Employees HTTP/1.1" + CRLF + + "Content-length: 100000" + CRLF + + "Content-type: application/octet-stream" + CRLF + + CRLF + + content + + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + "--batch_8194-cf13-1f56--"; + final List BatchRequestParts = parse(batch); + + for (BatchRequestPart part : BatchRequestParts) { + if (!part.isChangeSet()) { + assertEquals(1, part.getRequests().size()); + final ODataRequest retrieveRequest = part.getRequests().get(0); + + assertEquals(HttpMethod.GET, retrieveRequest.getMethod()); + assertEquals("http://localhost/odata/Employees?$filter=Age%20gt%2040", retrieveRequest.getRawRequestUri()); + assertEquals("http://localhost/odata", retrieveRequest.getRawBaseUri()); + assertEquals("/Employees", retrieveRequest.getRawODataPath()); + assertEquals("$filter=Age%20gt%2040", retrieveRequest.getRawQueryPath()); + } else { + final List requests = part.getRequests(); + for (ODataRequest request : requests) { + assertEquals(HttpMethod.POST, request.getMethod()); + assertEquals("100000", request.getHeader(HttpHeader.CONTENT_LENGTH)); + assertEquals("1", request.getHeader(BatchParserCommon.HTTP_CONTENT_ID)); + assertEquals("application/octet-stream", request.getHeader(HttpHeader.CONTENT_TYPE)); + + final InputStream body = request.getBody(); + assertEquals(content, StringUtil.toString(body)); + } + } + } + } + + @Test + public void testPostWithoutBody() throws IOException, BatchException, URISyntaxException { + final String batch = CRLF + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-ID: changeRequest1" + CRLF + + CRLF + + "POST Employees('2') HTTP/1.1" + CRLF + + "Content-Length: 100" + CRLF + + "Content-Type: application/octet-stream" + CRLF + + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + final List batchRequestParts = parse(batch); + + for (BatchRequestPart object : batchRequestParts) { + if (object.isChangeSet()) { + final List requests = object.getRequests(); + + for (ODataRequest request : requests) { + assertEquals(HttpMethod.POST, request.getMethod()); + assertEquals("100", request.getHeader(HttpHeader.CONTENT_LENGTH)); + assertEquals("application/octet-stream", request.getHeader(HttpHeader.CONTENT_TYPE)); + assertNotNull(request.getBody()); + } + } + } + } + + @Test + public void testBoundaryParameterWithQuotas() throws BatchException, UnsupportedEncodingException { + final String contentType = "multipart/mixed; boundary=\"batch_1.2+34:2j)0?\""; + final String batch = "" + + "--batch_1.2+34:2j)0?" + CRLF + + GET_REQUEST + + "--batch_1.2+34:2j)0?--"; + final BatchParser parser = new BatchParser(contentType, SERVICE_ROOT, "", true); + final List batchRequestParts = parser.parseBatchRequest(StringUtil.toInputStream(batch)); + + assertNotNull(batchRequestParts); + assertFalse(batchRequestParts.isEmpty()); + } + + @Test + public void testBatchWithInvalidContentType() throws UnsupportedEncodingException { + final String invalidContentType = "multipart;boundary=batch_1740-bb84-2f7f"; + final String batch = "" + + "--batch_1740-bb84-2f7f" + CRLF + + GET_REQUEST + + "--batch_1740-bb84-2f7f--"; + final BatchParser parser = new BatchParser(invalidContentType, SERVICE_ROOT, "", true); + + try { + parser.parseBatchRequest(StringUtil.toInputStream(batch)); + fail(); + } catch (BatchException e) { + assertMessageKey(e, BatchException.MessageKeys.INVALID_CONTENT_TYPE); + } + } + + @Test + public void testContentTypeCharset() throws BatchException { + final String contentType = "multipart/mixed; charset=UTF-8;boundary=batch_14d1-b293-b99a"; + final String batch = "" + + "--batch_14d1-b293-b99a" + CRLF + + GET_REQUEST + + "--batch_14d1-b293-b99a--"; + final BatchParser parser = new BatchParser(contentType, SERVICE_ROOT, "", true); + final List parts = parser.parseBatchRequest(StringUtil.toInputStream(batch)); + + assertEquals(1, parts.size()); + } + + @Test + public void testBatchWithoutBoundaryParameter() throws UnsupportedEncodingException { + final String invalidContentType = "multipart/mixed"; + final String batch = "" + + "--batch_1740-bb84-2f7f" + CRLF + + GET_REQUEST + + "--batch_1740-bb84-2f7f--"; + final BatchParser parser = new BatchParser(invalidContentType, SERVICE_ROOT, "", true); + + try { + parser.parseBatchRequest(StringUtil.toInputStream(batch)); + fail(); + } catch (BatchException e) { + assertMessageKey(e, BatchException.MessageKeys.INVALID_CONTENT_TYPE); + } + } + + @Test + public void testBoundaryParameterWithoutQuota() throws UnsupportedEncodingException { + final String invalidContentType = "multipart/mixed;boundary=batch_1740-bb:84-2f7f"; + final String batch = "" + + "--batch_1740-bb:84-2f7f" + CRLF + + GET_REQUEST + + "--batch_1740-bb:84-2f7f--"; + final BatchParser parser = new BatchParser(invalidContentType, SERVICE_ROOT, "", true); + + try { + parser.parseBatchRequest(StringUtil.toInputStream(batch)); + fail(); + } catch (BatchException e) { + assertMessageKey(e, BatchException.MessageKeys.INVALID_BOUNDARY); + } + } + + @Test + public void testWrongBoundaryString() throws BatchException, UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f5" + CRLF + + GET_REQUEST + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_BOUNDARY_DELIMITER); + } + + @Test + public void testMissingHttpVersion() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: application/http" + CRLF + + "Content-Transfer-Encoding:binary" + CRLF + + CRLF + + "GET Employees?$format=json" + CRLF + + "Host: localhost:8080" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_STATUS_LINE); + } + + @Test + public void testMissingHttpVersion2() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: application/http" + CRLF + + "Content-Transfer-Encoding:binary" + CRLF + + CRLF + + "GET Employees?$format=json " + CRLF + + "Host: localhost:8080" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_HTTP_VERSION); + } + + @Test + public void testMissingHttpVersion3() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: application/http" + CRLF + + "Content-Transfer-Encoding:binary" + CRLF + + CRLF + + "GET Employees?$format=json SMTP:3.1" + CRLF + + "Host: localhost:8080" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_HTTP_VERSION); + } + + @Test + public void testBoundaryWithoutHyphen() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + GET_REQUEST + + "batch_8194-cf13-1f56" + CRLF + + GET_REQUEST + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CONTENT); + } + + @Test + public void testNoBoundaryString() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + GET_REQUEST + // + no boundary string + + GET_REQUEST + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CONTENT); + } + + @Test + public void testBatchBoundaryEqualsChangeSetBoundary() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed;boundary=batch_8194-cf13-1f56" + CRLF + + CRLF + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "PUT Employees('2')/EmployeeName HTTP/1.1" + CRLF + + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "MaxDataServiceVersion: 2.0" + CRLF + + CRLF + + "{\"EmployeeName\":\"Frederic Fall MODIFIED\"}" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--" + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_BLANK_LINE); + } + + @Test + public void testNoContentType() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Transfer-Encoding: binary" + CRLF + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CONTENT_TYPE); + } + + @Test + public void testMimeHeaderContentType() throws UnsupportedEncodingException { + final String batch = "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: text/plain" + CRLF + + "Content-Transfer-Encoding: binary" + CRLF + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CONTENT_TYPE); + } + + @Test + public void testMimeHeaderEncoding() throws UnsupportedEncodingException { + String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: application/http" + CRLF + + "Content-Transfer-Encoding: 8bit" + CRLF + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CONTENT_TRANSFER_ENCODING); + } + + @Test + public void testGetRequestMissingCRLF() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + "Content-ID: 1" + CRLF + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF + // + CRLF // Belongs to the GET request + + CRLF // Belongs to the + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_BLANK_LINE); + } + + @Test + public void testInvalidMethodForBatch() throws UnsupportedEncodingException { + final String batch = "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "POST Employees('1')/EmployeeName HTTP/1.1" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_QUERY_OPERATION_METHOD); + } + + @Test + public void testNoBoundaryFound() throws UnsupportedEncodingException { + final String batch = "batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "POST Employees('1')/EmployeeName HTTP/1.1" + CRLF + + CRLF; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_BOUNDARY_DELIMITER); + } + + @Test + public void testBadRequest() throws UnsupportedEncodingException { + final String batch = "This is a bad request. There is no syntax and also no semantic"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_BOUNDARY_DELIMITER); + } + + @Test + public void testNoMethod() throws UnsupportedEncodingException { + final String batch = "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + /* GET */"Employees('1')/EmployeeName HTTP/1.1" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_STATUS_LINE); + } + + @Test + public void testInvalidMethodForChangeset() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-Id: 1" + CRLF + + CRLF + + "GET Employees('2')/EmployeeName HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "MaxDataServiceVersion: 2.0" + CRLF + + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd--" + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CHANGESET_METHOD); + } + + @Test + public void testInvalidChangeSetBoundary() throws UnsupportedEncodingException { + final String batch = "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94d"/* +"d" */+ CRLF + + MIME_HEADERS + + CRLF + + "POST Employees('2') HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "MaxDataServiceVersion: 2.0" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_BOUNDARY_DELIMITER); + } + + @Test + public void testNestedChangeset() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + "Content-Transfer-Encoding: binary" + CRLF + + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd2" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd2" + CRLF + + MIME_HEADERS + + "Content-Id: 1" + CRLF + + CRLF + + "POST Employees('2') HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "MaxDataServiceVersion: 2.0" + CRLF + + "Content-Id: 2" + + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_CONTENT_TYPE); + } + + @Test + public void testMissingContentTransferEncoding() throws UnsupportedEncodingException { + final String batch = "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + "Content-Id: 1" + CRLF + + "Content-Type: application/http" + CRLF + // + "Content-Transfer-Encoding: binary" + CRLF + + CRLF + + "POST Employees('2') HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "MaxDataServiceVersion: 2.0" + CRLF + + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CONTENT_TRANSFER_ENCODING); + } + + @Test + public void testMissingContentType() throws UnsupportedEncodingException { + final String batch = "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + "Content-Id: 1" + // + "Content-Type: application/http" + CRLF + + "Content-Transfer-Encoding: binary" + CRLF + + CRLF + + "POST Employees('2') HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "MaxDataServiceVersion: 2.0" + CRLF + + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CONTENT_TYPE); + } + + @Test + public void testNoCloseDelimiter() throws BatchException, UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + GET_REQUEST; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CLOSE_DELIMITER); + } + + @Test + public void testNoCloseDelimiter2() throws BatchException, UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CLOSE_DELIMITER); + } + + @Test + public void testInvalidUri() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET http://localhost/aa/odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_URI); + } + + @Test + public void testUriWithAbsolutePath() throws BatchException, UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET /odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF + + "Host: http://localhost" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + final List parts = parse(batch); + assertEquals(1, parts.size()); + + final BatchRequestPart part = parts.get(0); + assertEquals(1, part.getRequests().size()); + final ODataRequest request = part.getRequests().get(0); + + assertEquals("http://localhost/odata/Employees('1')/EmployeeName", request.getRawRequestUri()); + assertEquals("http://localhost/odata", request.getRawBaseUri()); + assertEquals("/Employees('1')/EmployeeName", request.getRawODataPath()); + assertEquals("", request.getRawQueryPath()); + } + + @Test + public void testUriWithAbsolutePathMissingHostHeader() throws BatchException, UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET /odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, MessageKeys.MISSING_MANDATORY_HEADER); + } + + @Test + public void testUriWithAbsolutePathMissingHostDulpicatedHeader() throws BatchException, UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET /odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF + + "Host: http://localhost" + CRLF + + "Host: http://localhost/odata" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, MessageKeys.MISSING_MANDATORY_HEADER); + } + + @Test + public void testUriWithAbsolutePathOtherHost() throws BatchException, UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET /odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF + + "Host: http://localhost2" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, MessageKeys.INVALID_URI); + } + + @Test + public void testUriWithAbsolutePathWrongPath() throws BatchException, UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET /myservice/Employees('1')/EmployeeName HTTP/1.1" + CRLF + + "Host: http://localhost" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, MessageKeys.INVALID_URI); + } + + @Test + public void testNoCloseDelimiter3() throws UnsupportedEncodingException { + final String batch = "--batch_8194-cf13-1f56" + CRLF + GET_REQUEST + "--batch_8194-cf13-1f56-"/* no hyphen */; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.MISSING_CLOSE_DELIMITER); + } + + @Test + public void testNegativeContentLengthChangeSet() throws BatchException, IOException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-ID: 1" + CRLF + + "Content-Length: -2" + CRLF + + CRLF + + "PUT EmployeeName HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "Content-Id: 1" + CRLF + + CRLF + + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parse(batch); + } + + @Test + public void testNegativeContentLengthRequest() throws BatchException, IOException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-ID: 1" + CRLF + + CRLF + + "PUT EmployeeName HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "Content-Id: 1" + CRLF + + "Content-Length: 2" + CRLF + + CRLF + + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parse(batch); + } + + @Test + public void testContentLengthGreatherThanBodyLength() throws BatchException, IOException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-ID: 1" + CRLF + + CRLF + + "PUT Employee/Name HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "Content-Length: 100000" + CRLF + + CRLF + + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + final List batchRequestParts = parse(batch); + + assertNotNull(batchRequestParts); + + for (BatchRequestPart multipart : batchRequestParts) { + if (multipart.isChangeSet()) { + assertEquals(1, multipart.getRequests().size()); + + final ODataRequest request = multipart.getRequests().get(0); + assertEquals("{\"EmployeeName\":\"Peter Fall\"}", StringUtil.toString(request.getBody())); + } + } + } + + @Test + public void testContentLengthSmallerThanBodyLength() throws BatchException, IOException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-ID: 1" + CRLF + + CRLF + + "PUT EmployeeName HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "Content-Length: 10" + CRLF + + CRLF + + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + final List batchRequestParts = parse(batch); + + assertNotNull(batchRequestParts); + + for (BatchRequestPart multipart : batchRequestParts) { + if (multipart.isChangeSet()) { + assertEquals(1, multipart.getRequests().size()); + + final ODataRequest request = multipart.getRequests().get(0); + assertEquals("{\"Employee", StringUtil.toString(request.getBody())); + } + } + } + + @Test + public void testNonNumericContentLength() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-ID: 1" + CRLF + + CRLF + + "PUT EmployeeName HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "Content-Length: 10abc" + CRLF + + CRLF + + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_HEADER); + } + + @Test + public void testNonStrictParser() throws BatchException, IOException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed;boundary=changeset_8194-cf13-1f56" + CRLF + + "--changeset_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + "Content-ID: myRequest" + CRLF + + "PUT Employees('2')/EmployeeName HTTP/1.1" + CRLF + + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "MaxDataServiceVersion: 2.0" + CRLF + + "{\"EmployeeName\":\"Frederic Fall MODIFIED\"}" + CRLF + + "--changeset_8194-cf13-1f56--" + CRLF + + "--batch_8194-cf13-1f56--"; + + final List requests = parse(batch, false); + + assertNotNull(requests); + assertEquals(1, requests.size()); + + final BatchRequestPart part = requests.get(0); + assertTrue(part.isChangeSet()); + assertNotNull(part.getRequests()); + assertEquals(1, part.getRequests().size()); + + final ODataRequest changeRequest = part.getRequests().get(0); + assertEquals("{\"EmployeeName\":\"Frederic Fall MODIFIED\"}", + StringUtil.toString(changeRequest.getBody())); + assertEquals("application/json;odata=verbose", changeRequest.getHeader(HttpHeader.CONTENT_TYPE)); + assertEquals(HttpMethod.PUT, changeRequest.getMethod()); + } + + @Test + public void testNonStrictParserMoreCRLF() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed;boundary=changeset_8194-cf13-1f56" + CRLF + + "--changeset_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + CRLF // Only one CRLF allowed + + "PUT Employees('2')/EmployeeName HTTP/1.1" + CRLF + + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "MaxDataServiceVersion: 2.0" + CRLF + + "{\"EmployeeName\":\"Frederic Fall MODIFIED\"}" + CRLF + + "--changeset_8194-cf13-1f56--" + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, BatchException.MessageKeys.INVALID_STATUS_LINE, false); + } + + @Test + public void testContentId() throws BatchException, UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees HTTP/1.1" + CRLF + + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF + + "Content-Id: BBB" + CRLF + + CRLF + CRLF + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-Id: 1" + CRLF + + CRLF + + "POST Employees HTTP/1.1" + CRLF + + "Content-type: application/octet-stream" + CRLF + + CRLF + + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + CRLF + + "PUT $1/EmployeeName HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + "Content-Id: 2" + CRLF + + CRLF + + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + final List batchRequestParts = parse(batch); + assertNotNull(batchRequestParts); + + for (BatchRequestPart multipart : batchRequestParts) { + if (!multipart.isChangeSet()) { + assertEquals(1, multipart.getRequests().size()); + final ODataRequest retrieveRequest = multipart.getRequests().get(0); + + assertEquals("BBB", retrieveRequest.getHeader(BatchParserCommon.HTTP_CONTENT_ID)); + } else { + for (ODataRequest request : multipart.getRequests()) { + if (HttpMethod.POST.equals(request.getMethod())) { + assertEquals("1", request.getHeader(BatchParserCommon.HTTP_CONTENT_ID)); + } else if (HttpMethod.PUT.equals(request.getMethod())) { + assertEquals("2", request.getHeader(BatchParserCommon.HTTP_CONTENT_ID)); + assertEquals("/$1/EmployeeName", request.getRawODataPath()); + assertEquals("http://localhost/odata/$1/EmployeeName", request.getRawRequestUri()); + } + } + } + } + } + + @Test + public void testNoContentId() throws BatchException, UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees HTTP/1.1" + CRLF + + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF + + CRLF + CRLF + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-Id: 1" + CRLF + + CRLF + + "POST Employees HTTP/1.1" + CRLF + + "Content-type: application/octet-stream" + CRLF + + CRLF + + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-Id: 1" + CRLF + + CRLF + + "PUT $1/EmployeeName HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + CRLF + + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parse(batch); + } + + @Test + public void testPreamble() throws BatchException, IOException { + final String batch = "" + + "This is a preamble and must be ignored" + CRLF + + CRLF + + CRLF + + "----1242" + CRLF + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees HTTP/1.1" + CRLF + + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF + + "Content-Id: BBB" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "This is a preamble and must be ignored" + CRLF + + CRLF + + CRLF + + "----1242" + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-Id: 1" + CRLF + + CRLF + + "POST Employees HTTP/1.1" + CRLF + + "Content-type: application/octet-stream" + CRLF + + CRLF + + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-ID: 2" + CRLF + + CRLF + + "PUT $1/EmployeeName HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + CRLF + + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + final List batchRequestParts = parse(batch); + + assertNotNull(batchRequestParts); + assertEquals(2, batchRequestParts.size()); + + final BatchRequestPart getRequestPart = batchRequestParts.get(0); + assertEquals(1, getRequestPart.getRequests().size()); + + final ODataRequest getRequest = getRequestPart.getRequests().get(0); + assertEquals(HttpMethod.GET, getRequest.getMethod()); + + final BatchRequestPart changeSetPart = batchRequestParts.get(1); + assertEquals(2, changeSetPart.getRequests().size()); + assertEquals("/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + + CRLF, + StringUtil.toString(changeSetPart.getRequests().get(0).getBody())); + assertEquals("{\"EmployeeName\":\"Peter Fall\"}", + StringUtil.toString(changeSetPart.getRequests().get(1).getBody())); + } + + @Test + public void testContentTypeCaseInsensitive() throws BatchException, IOException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: muLTiParT/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-ID: 1" + CRLF + + "Content-Length: 200" + CRLF + + CRLF + + "PUT EmployeeName HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + CRLF + + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parse(batch); + } + + @Test + public void testContentTypeBoundaryCaseInsensitive() throws BatchException, IOException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; bOunDaRy=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-ID: 1" + CRLF + + CRLF + + "PUT EmployeeName HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + CRLF + + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + final List batchRequestParts = parse(batch); + + assertNotNull(batchRequestParts); + assertEquals(1, batchRequestParts.size()); + assertTrue(batchRequestParts.get(0).isChangeSet()); + assertEquals(1, batchRequestParts.get(0).getRequests().size()); + } + + @Test + public void testEpilog() throws BatchException, IOException { + String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees HTTP/1.1" + CRLF + + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF + + "Content-Id: BBB" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56" + CRLF + + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-Id: 1" + CRLF + + CRLF + + "POST Employees HTTP/1.1" + CRLF + + "Content-type: application/octet-stream" + CRLF + + CRLF + + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF + + CRLF + + "--changeset_f980-1cb6-94dd" + CRLF + + MIME_HEADERS + + "Content-ID: 2" + CRLF + + CRLF + + "PUT $1/EmployeeName HTTP/1.1" + CRLF + + "Content-Type: application/json;odata=verbose" + CRLF + + CRLF + + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF + + "--changeset_f980-1cb6-94dd--" + CRLF + + CRLF + + "This is an epilog and must be ignored" + CRLF + + CRLF + + CRLF + + "----1242" + + CRLF + + "--batch_8194-cf13-1f56--" + + CRLF + + "This is an epilog and must be ignored" + CRLF + + CRLF + + CRLF + + "----1242"; + final List batchRequestParts = parse(batch); + + assertNotNull(batchRequestParts); + assertEquals(2, batchRequestParts.size()); + + BatchRequestPart getRequestPart = batchRequestParts.get(0); + assertEquals(1, getRequestPart.getRequests().size()); + ODataRequest getRequest = getRequestPart.getRequests().get(0); + assertEquals(HttpMethod.GET, getRequest.getMethod()); + + BatchRequestPart changeSetPart = batchRequestParts.get(1); + assertEquals(2, changeSetPart.getRequests().size()); + assertEquals("/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + + CRLF, + StringUtil.toString(changeSetPart.getRequests().get(0).getBody())); + assertEquals("{\"EmployeeName\":\"Peter Fall\"}", + StringUtil.toString(changeSetPart.getRequests().get(1).getBody())); + } + + @Test + public void testLargeBatch() throws BatchException, IOException { + final InputStream in = readFile("/batchLarge.batch"); + parse(in); + } + + @Test + public void testForddenHeaderAuthorisation() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF + + "Authorization: Basic QWxhZdsdsddsduIHNlc2FtZQ==" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER); + } + + @Test + public void testForddenHeaderExpect() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF + + "Expect: 100-continue" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER); + } + + @Test + public void testForddenHeaderFrom() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF + + "From: test@test.com" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER); + } + + @Test + public void testForddenHeaderRange() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF + + "Range: 200-256" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER); + } + + @Test + public void testForddenHeaderMaxForwards() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF + + "Max-Forwards: 3" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER); + } + + @Test + public void testForddenHeaderTE() throws UnsupportedEncodingException { + final String batch = "" + + "--batch_8194-cf13-1f56" + CRLF + + MIME_HEADERS + + CRLF + + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF + + "TE: deflate" + CRLF + + CRLF + + CRLF + + "--batch_8194-cf13-1f56--"; + + parseInvalidBatchBody(batch, MessageKeys.FORBIDDEN_HEADER); + } + + private List parse(final InputStream in, final boolean isStrict) throws BatchException { + final BatchParser parser = new BatchParser(CONTENT_TYPE, SERVICE_ROOT, "", isStrict); + final List batchRequestParts = parser.parseBatchRequest(in); + + assertNotNull(batchRequestParts); + assertFalse(batchRequestParts.isEmpty()); + + return batchRequestParts; + } + + private List parse(final InputStream in) throws BatchException { + return parse(in, true); + } + + private List parse(final String batch) throws BatchException, UnsupportedEncodingException { + return parse(batch, true); + } + + private List parse(final String batch, final boolean isStrict) throws BatchException, + UnsupportedEncodingException { + return parse(StringUtil.toInputStream(batch), isStrict); + } + + private void parseInvalidBatchBody(final String batch, final MessageKeys key, final boolean isStrict) + throws UnsupportedEncodingException { + final BatchParser parser = new BatchParser(CONTENT_TYPE, SERVICE_ROOT, "", isStrict); + + try { + parser.parseBatchRequest(StringUtil.toInputStream(batch)); + fail("No exception thrown. Expect: " + key.toString()); + } catch (BatchException e) { + assertMessageKey(e, key); + } + } + + private void parseInvalidBatchBody(final String batch, final MessageKeys key) throws UnsupportedEncodingException { + parseInvalidBatchBody(batch, key, true); + } + + private void assertMessageKey(final BatchException e, final MessageKeys key) { + assertEquals(key, e.getMessageKey()); + } + + private InputStream readFile(final String fileName) throws IOException { + final InputStream in = ClassLoader.class.getResourceAsStream(fileName); + if (in == null) { + throw new IOException("Requested file '" + fileName + "' was not found."); + } + return in; + } +} diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/BatchParserCommonTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/BatchParserCommonTest.java new file mode 100644 index 000000000..6fb079670 --- /dev/null +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/BatchParserCommonTest.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.server.core.batch.BatchException; +import org.apache.olingo.server.core.batch.parser.BatchParserCommon; +import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings.Line; +import org.apache.olingo.server.core.batch.parser.Header; +import org.junit.Test; + +public class BatchParserCommonTest { + + private static final String CRLF = "\r\n"; + + @Test + public void testMultipleHeader() throws BatchException { + String[] messageRaw = new String[] { + "Content-Id: 1" + CRLF, + "Content-Id: 2" + CRLF, + "content-type: Application/http" + CRLF, + "content-transfer-encoding: Binary" + CRLF + }; + List message = toLineList(messageRaw); + + final Header header = BatchParserCommon.consumeHeaders(message); + assertNotNull(header); + + final List contentIdHeaders = header.getHeaders(BatchParserCommon.HTTP_CONTENT_ID); + assertNotNull(contentIdHeaders); + assertEquals(2, contentIdHeaders.size()); + assertEquals("1", contentIdHeaders.get(0)); + assertEquals("2", contentIdHeaders.get(1)); + } + + @Test + public void testMultipleHeaderSameValue() throws BatchException { + String[] messageRaw = new String[] { + "Content-Id: 1" + CRLF, + "Content-Id: 1" + CRLF, + "content-type: Application/http" + CRLF, + "content-transfer-encoding: Binary" + CRLF + }; + List message = toLineList(messageRaw); + + final Header header = BatchParserCommon.consumeHeaders(message); + assertNotNull(header); + + final List contentIdHeaders = header.getHeaders(BatchParserCommon.HTTP_CONTENT_ID); + assertNotNull(contentIdHeaders); + assertEquals(1, contentIdHeaders.size()); + assertEquals("1", contentIdHeaders.get(0)); + } + + @Test + public void testHeaderSperatedByComma() throws BatchException { + String[] messageRaw = new String[] { + "Content-Id: 1" + CRLF, + "Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11" + CRLF, + "content-type: Application/http" + CRLF, + "content-transfer-encoding: Binary" + CRLF + }; + List message = toLineList(messageRaw); + + final Header header = BatchParserCommon.consumeHeaders(message); + assertNotNull(header); + + final List upgradeHeader = header.getHeaders("upgrade"); + assertNotNull(upgradeHeader); + assertEquals(4, upgradeHeader.size()); + assertEquals("HTTP/2.0", upgradeHeader.get(0)); + assertEquals("SHTTP/1.3", upgradeHeader.get(1)); + assertEquals("IRC/6.9", upgradeHeader.get(2)); + assertEquals("RTA/x11", upgradeHeader.get(3)); + } + + @Test + public void testMultipleAcceptHeader() throws BatchException { + String[] messageRaw = new String[] { + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF, + "Accept: text/plain;q=0.3" + CRLF, + "Accept-Language:en-US,en;q=0.7,en-UK;q=0.9" + CRLF, + "content-type: Application/http" + CRLF, + "content-transfer-encoding: Binary" + CRLF + }; + List message = toLineList(messageRaw); + + final Header header = BatchParserCommon.consumeHeaders(message); + assertNotNull(header); + + final List acceptHeader = header.getHeaders(HttpHeader.ACCEPT); + assertNotNull(acceptHeader); + assertEquals(4, acceptHeader.size()); + } + + @Test + public void testMultipleAcceptHeaderSameValue() throws BatchException { + String[] messageRaw = new String[] { + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF, + "Accept: application/atomsvc+xml;q=0.8" + CRLF, + "Accept-Language:en-US,en;q=0.7,en-UK;q=0.9" + CRLF, + "content-type: Application/http" + CRLF, + "content-transfer-encoding: Binary" + CRLF + }; + List message = toLineList(messageRaw); + + final Header header = BatchParserCommon.consumeHeaders(message); + assertNotNull(header); + + final List acceptHeader = header.getHeaders(HttpHeader.ACCEPT); + assertNotNull(acceptHeader); + assertEquals(3, acceptHeader.size()); + } + + @Test + public void testMultipleAccepLanguagetHeader() throws BatchException { + String[] messageRaw = new String[] { + "Accept-Language:en-US,en;q=0.7,en-UK;q=0.9" + CRLF, + "Accept-Language: de-DE;q=0.3" + CRLF, + "content-type: Application/http" + CRLF, + "content-transfer-encoding: Binary" + CRLF + }; + List message = toLineList(messageRaw); + + final Header header = BatchParserCommon.consumeHeaders(message); + assertNotNull(header); + + final List acceptLanguageHeader = header.getHeaders(HttpHeader.ACCEPT_LANGUAGE); + assertNotNull(acceptLanguageHeader); + assertEquals(4, acceptLanguageHeader.size()); + } + + @Test + public void testMultipleAccepLanguagetHeaderSameValue() throws BatchException { + String[] messageRaw = new String[] { + "Accept-Language:en-US,en;q=0.7,en-UK;q=0.9" + CRLF, + "Accept-Language:en-US,en;q=0.7" + CRLF, + "content-type: Application/http" + CRLF, + "content-transfer-encoding: Binary" + CRLF + }; + List message = toLineList(messageRaw); + + final Header header = BatchParserCommon.consumeHeaders(message); + assertNotNull(header); + + final List acceptLanguageHeader = header.getHeaders(HttpHeader.ACCEPT_LANGUAGE); + assertNotNull(acceptLanguageHeader); + assertEquals(3, acceptLanguageHeader.size()); + } + + @Test + public void testRemoveEndingCRLF() { + String line = "Test\r\n"; + assertEquals("Test", BatchParserCommon.removeEndingCRLF(new Line(line,1)).toString()); + } + + @Test + public void testRemoveLastEndingCRLF() { + String line = "Test\r\n\r\n"; + assertEquals("Test\r\n", BatchParserCommon.removeEndingCRLF(new Line(line,1)).toString()); + } + + @Test + public void testRemoveEndingCRLFWithWS() { + String line = "Test\r\n "; + assertEquals("Test", BatchParserCommon.removeEndingCRLF(new Line(line,1)).toString()); + } + + @Test + public void testRemoveEndingCRLFNothingToRemove() { + String line = "Hallo\r\nBla"; + assertEquals("Hallo\r\nBla", BatchParserCommon.removeEndingCRLF(new Line(line,1)).toString()); + } + + @Test + public void testRemoveEndingCRLFAll() { + String line = "\r\n"; + assertEquals("", BatchParserCommon.removeEndingCRLF(new Line(line,1)).toString()); + } + + @Test + public void testRemoveEndingCRLFSpace() { + String line = "\r\n "; + assertEquals("", BatchParserCommon.removeEndingCRLF(new Line(line,1)).toString()); + } + + @Test + public void testRemoveLastEndingCRLFWithWS() { + String line = "Test \r\n"; + assertEquals("Test ", BatchParserCommon.removeEndingCRLF(new Line(line,1)).toString()); + } + + @Test + public void testRemoveLastEndingCRLFWithWSLong() { + String line = "Test \r\nTest2 \r\n"; + assertEquals("Test \r\nTest2 ", BatchParserCommon.removeEndingCRLF(new Line(line,1)).toString()); + } + + private List toLineList(String[] messageRaw) { + final List lineList = new ArrayList(); + int counter = 1; + + for(final String currentLine : messageRaw) { + lineList.add(new Line(currentLine, counter++)); + } + + return lineList; + } +} diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/BufferedReaderIncludingLineEndingsTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/BufferedReaderIncludingLineEndingsTest.java new file mode 100644 index 000000000..eac9dffaa --- /dev/null +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/BufferedReaderIncludingLineEndingsTest.java @@ -0,0 +1,484 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import static org.junit.Assert.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.List; + +import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings; +import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings.Line; +import org.junit.Test; + +public class BufferedReaderIncludingLineEndingsTest { + + + private static final String TEXT_COMBINED = "Test\r" + + "Test2\r\n" + + "Test3\n" + + "Test4\r" + + "\r" + + "\r\n" + + "\r\n" + + "Test5\n" + + "Test6\r\n" + + "Test7\n" + + "\n"; + + private static final String TEXT_SMALL = "Test\r" + + "123"; + private static final String TEXT_EMPTY = ""; + + @Test + public void testSimpleText() throws IOException { + final String TEXT = "Test"; + BufferedReaderIncludingLineEndings reader = create(TEXT); + + assertEquals(TEXT, reader.readLine()); + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testNoText() throws IOException { + final String TEXT = ""; + BufferedReaderIncludingLineEndings reader = create(TEXT); + + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testNoBytes() throws IOException { + BufferedReaderIncludingLineEndings reader = + new BufferedReaderIncludingLineEndings(new InputStreamReader(new ByteArrayInputStream(new byte[0]))); + + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testCRLF() throws IOException { + final String TEXT = "Test\r\n" + + "Test2"; + + BufferedReaderIncludingLineEndings reader = create(TEXT); + + assertEquals("Test\r\n", reader.readLine()); + assertEquals("Test2", reader.readLine()); + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testLF() throws IOException { + final String TEXT = "Test\n" + + "Test2"; + + BufferedReaderIncludingLineEndings reader = create(TEXT); + + assertEquals("Test\n", reader.readLine()); + assertEquals("Test2", reader.readLine()); + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testCR() throws IOException { + final String TEXT = "Test\r" + + "Test2"; + + BufferedReaderIncludingLineEndings reader = create(TEXT); + + assertEquals("Test\r", reader.readLine()); + assertEquals("Test2", reader.readLine()); + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testCombined() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_COMBINED); + + assertEquals("Test\r", reader.readLine()); + assertEquals("Test2\r\n", reader.readLine()); + assertEquals("Test3\n", reader.readLine()); + assertEquals("Test4\r", reader.readLine()); + assertEquals("\r", reader.readLine()); + assertEquals("\r\n", reader.readLine()); + assertEquals("\r\n", reader.readLine()); + assertEquals("Test5\n", reader.readLine()); + assertEquals("Test6\r\n", reader.readLine()); + assertEquals("Test7\n", reader.readLine()); + assertEquals("\n", reader.readLine()); + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testCombinedBufferSizeTwo() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_COMBINED, 2); + + assertEquals("Test\r", reader.readLine()); + assertEquals("Test2\r\n", reader.readLine()); + assertEquals("Test3\n", reader.readLine()); + assertEquals("Test4\r", reader.readLine()); + assertEquals("\r", reader.readLine()); + assertEquals("\r\n", reader.readLine()); + assertEquals("\r\n", reader.readLine()); + assertEquals("Test5\n", reader.readLine()); + assertEquals("Test6\r\n", reader.readLine()); + assertEquals("Test7\n", reader.readLine()); + assertEquals("\n", reader.readLine()); + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testCombinedBufferSizeOne() throws IOException { + final String TEXT = "Test\r" + + "Test2\r\n" + + "Test3\n" + + "Test4\r" + + "\r" + + "\r\n" + + "\r\n" + + "Test5\n" + + "Test6\r\n" + + "Test7\n" + + "\r\n"; + + BufferedReaderIncludingLineEndings reader = create(TEXT, 1); + + assertEquals("Test\r", reader.readLine()); + assertEquals("Test2\r\n", reader.readLine()); + assertEquals("Test3\n", reader.readLine()); + assertEquals("Test4\r", reader.readLine()); + assertEquals("\r", reader.readLine()); + assertEquals("\r\n", reader.readLine()); + assertEquals("\r\n", reader.readLine()); + assertEquals("Test5\n", reader.readLine()); + assertEquals("Test6\r\n", reader.readLine()); + assertEquals("Test7\n", reader.readLine()); + assertEquals("\r\n", reader.readLine()); + assertNull(reader.readLine()); + assertNull(reader.readLine()); + + reader.close(); + } + + @Test + public void testDoubleLF() throws IOException { + final String TEXT = "Test\r" + + "\r"; + + BufferedReaderIncludingLineEndings reader = create(TEXT, 1); + + assertEquals("Test\r", reader.readLine()); + assertEquals("\r", reader.readLine()); + reader.close(); + } + + @Test + public void testSkipSimple() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL); + + assertEquals(5, reader.skip(5)); // Test\r + assertEquals("123", reader.readLine()); + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testSkipBufferOne() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL, 1); + + assertEquals(5, reader.skip(5)); // Test\r + assertEquals("123", reader.readLine()); + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testReadThanSkip() throws IOException { + final String TEXT = "Test\r" + + "\r" + + "123"; + + BufferedReaderIncludingLineEndings reader = create(TEXT); + + assertEquals("Test\r", reader.readLine()); + assertEquals(1, reader.skip(1)); // Test\r + assertEquals("123", reader.readLine()); + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testReadMoreBufferCapacityThanCharacterAvailable() throws IOException { + final String TEXT = "Foo"; + char[] buffer = new char[20]; + + BufferedReaderIncludingLineEndings reader = create(TEXT); + assertEquals(3, reader.read(buffer, 0, 20)); + assertEquals(-1, reader.read(buffer, 0, 20)); + reader.close(); + + BufferedReaderIncludingLineEndings readerBufferOne = create(TEXT, 1); + assertEquals(3, readerBufferOne.read(buffer, 0, 20)); + assertEquals(-1, readerBufferOne.read(buffer, 0, 20)); + readerBufferOne.close(); + } + + @Test + public void testSkipZero() throws IOException { + final String TEXT = "Test\r" + + "123\r\n"; + + BufferedReaderIncludingLineEndings reader = create(TEXT); + + assertEquals(0, reader.skip(0)); // Test\r + assertEquals("Test\r", reader.readLine()); + assertEquals("123\r\n", reader.readLine()); + assertNull(reader.readLine()); + assertNull(reader.readLine()); + reader.close(); + } + + @Test + public void testSkipToMuch() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL); + + assertEquals(8, reader.skip(10)); // Test\r + assertEquals(null, reader.readLine()); + reader.close(); + } + + @Test + public void testReadBufferOne() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL, 1); + + assertEquals('T', reader.read()); + assertEquals('e', reader.read()); + assertEquals('s', reader.read()); + assertEquals('t', reader.read()); + assertEquals('\r', reader.read()); + assertEquals('1', reader.read()); + assertEquals('2', reader.read()); + assertEquals('3', reader.read()); + assertEquals(-1, reader.read()); + assertEquals(-1, reader.read()); + } + + @Test + public void testReadZeroBytes() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL, 1); + + char[] buffer = new char[3]; + assertEquals(0, reader.read(buffer, 0, 0)); + assertEquals('T', reader.read()); + assertEquals(0, reader.read(buffer, 0, 0)); + assertEquals("est\r", reader.readLine()); + assertEquals("123", reader.readLine()); + + reader.close(); + } + + @Test + public void testRead() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_SMALL); + + assertEquals('T', reader.read()); + assertEquals('e', reader.read()); + assertEquals('s', reader.read()); + assertEquals('t', reader.read()); + assertEquals('\r', reader.read()); + assertEquals('1', reader.read()); + assertEquals('2', reader.read()); + assertEquals('3', reader.read()); + assertEquals(-1, reader.read()); + assertEquals(-1, reader.read()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testFailReadBufferAndOffsetBiggerThanBuffer() throws IOException { + BufferedReaderIncludingLineEndings reader = create(""); + + final char[] buffer = new char[3]; + reader.read(buffer, 1, 3); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testFailLengthNegative() throws IOException { + final char[] buffer = new char[3]; + BufferedReaderIncludingLineEndings reader = create("123"); + + reader.read(buffer, 1, -2); + reader.close(); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testFailOffsetNegative() throws IOException { + final char[] buffer = new char[3]; + BufferedReaderIncludingLineEndings reader = create("123"); + + reader.read(buffer, -1, 2); + reader.close(); + } + + @Test + public void testReadAndReadLine() throws IOException { + final String TEXT = "Test\r" + + "bar\n" + + "123\r\n" + + "foo"; + + BufferedReaderIncludingLineEndings reader = create(TEXT); + + assertEquals('T', reader.read()); + assertEquals('e', reader.read()); + assertEquals('s', reader.read()); + assertEquals('t', reader.read()); + assertEquals("\r", reader.readLine()); + assertEquals("bar\n", reader.readLine()); + assertEquals('1', reader.read()); + assertEquals('2', reader.read()); + assertEquals("3\r\n", reader.readLine()); + assertEquals("foo", reader.readLine()); + assertEquals(null, reader.readLine()); + assertEquals(-1, reader.read()); + } + + @Test + public void testLineEqualsAndHashCode() { + Line l1 = new Line("The first line", 1); + Line l2 = new Line("The first line", 1); + Line l3 = new Line("The second line", 2); + + assertEquals(l1, l2); + assertFalse(l1.equals(l3)); + assertTrue(l1.hashCode() != l3.hashCode()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSkipNegative() throws IOException { + BufferedReaderIncludingLineEndings reader = create("123"); + reader.skip(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void testFailBufferSizeZero() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_EMPTY, 0); + reader.close(); + } + + @Test(expected = NullPointerException.class) + public void testInputStreamIsNull() throws IOException { + // Same behaviour like BufferedReader + BufferedReaderIncludingLineEndings reader = new BufferedReaderIncludingLineEndings(null); + reader.close(); + } + + @Test(expected = IllegalArgumentException.class) + public void testFailBufferSizeNegative() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_EMPTY, -1); + reader.close(); + } + + @Test + public void testMarkSupoorted() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_EMPTY); + + assertEquals(false, reader.markSupported()); + reader.close(); + } + + @Test(expected = IOException.class) + public void testFailMark() throws IOException { + BufferedReaderIncludingLineEndings reader = create("123"); + + reader.mark(1); + } + + @Test(expected = IOException.class) + public void testFailReset() throws IOException { + BufferedReaderIncludingLineEndings reader = create("123"); + + reader.reset(); + } + + @Test + public void testReady() throws IOException { + BufferedReaderIncludingLineEndings reader = create("123\r123"); + assertEquals(false, reader.ready()); + assertEquals("123\r", reader.readLine()); + assertEquals(true, reader.ready()); + assertEquals("123", reader.readLine()); + assertEquals(false, reader.ready()); + + reader.close(); + } + + @Test + public void testToList() throws IOException { + BufferedReaderIncludingLineEndings reader = create(TEXT_COMBINED); + List stringList = reader.toLineList(); + + assertEquals(11, stringList.size()); + assertEquals("Test\r", stringList.get(0).toString()); + assertEquals("Test2\r\n", stringList.get(1).toString()); + assertEquals("Test3\n", stringList.get(2).toString()); + assertEquals("Test4\r", stringList.get(3).toString()); + assertEquals("\r", stringList.get(4).toString()); + assertEquals("\r\n", stringList.get(5).toString()); + assertEquals("\r\n", stringList.get(6).toString()); + assertEquals("Test5\n", stringList.get(7).toString()); + assertEquals("Test6\r\n", stringList.get(8).toString()); + assertEquals("Test7\n", stringList.get(9).toString()); + assertEquals("\n", stringList.get(10).toString()); + reader.close(); + } + + private BufferedReaderIncludingLineEndings create(final String inputString) throws UnsupportedEncodingException { + return new BufferedReaderIncludingLineEndings(new InputStreamReader(new ByteArrayInputStream(inputString + .getBytes("UTF-8")))); + } + + private BufferedReaderIncludingLineEndings create(final String inputString, int bufferSize) + throws UnsupportedEncodingException { + return new BufferedReaderIncludingLineEndings(new InputStreamReader(new ByteArrayInputStream(inputString + .getBytes("UTF-8"))), bufferSize); + } + +} diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/HeaderTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/HeaderTest.java new file mode 100644 index 000000000..f38ac89a9 --- /dev/null +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/parser/HeaderTest.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.parser; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.List; + +import org.apache.olingo.commons.api.http.HttpContentType; +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.server.core.batch.parser.BatchParserCommon; +import org.apache.olingo.server.core.batch.parser.Header; +import org.junit.Test; + +public class HeaderTest { + + @Test + public void test() { + Header header = new Header(1); + header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1); + + assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeader(HttpHeader.CONTENT_TYPE)); + assertEquals(1, header.getHeaders(HttpHeader.CONTENT_TYPE).size()); + assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeaders(HttpHeader.CONTENT_TYPE).get(0)); + } + + @Test + public void testNotAvailable() { + Header header = new Header(1); + + assertNull(header.getHeader(HttpHeader.CONTENT_TYPE)); + assertEquals(0, header.getHeaders(HttpHeader.CONTENT_TYPE).size()); + assertEquals("", header.getHeaderNotNull(HttpHeader.CONTENT_TYPE)); + } + + @Test + public void testCaseInsensitive() { + Header header = new Header(1); + header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1); + + assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeader("cOnTenT-TyPE")); + assertEquals(1, header.getHeaders("cOnTenT-TyPE").size()); + assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeaders("cOnTenT-TyPE").get(0)); + } + + @Test + public void testDuplicatedAdd() { + Header header = new Header(1); + header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1); + header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 2); + + assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeader(HttpHeader.CONTENT_TYPE)); + assertEquals(1, header.getHeaders(HttpHeader.CONTENT_TYPE).size()); + assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeaders(HttpHeader.CONTENT_TYPE).get(0)); + } + + @Test + public void testMatcher() { + Header header = new Header(1); + header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED + ";boundary=123", 1); + + assertTrue(header.isHeaderMatching(HttpHeader.CONTENT_TYPE, BatchParserCommon.PATTERN_MULTIPART_BOUNDARY)); + } + + @Test + public void testFieldName() { + Header header = new Header(0); + header.addHeader("MyFieldNamE", "myValue", 1); + + assertEquals("MyFieldNamE", header.getHeaderField("myfieldname").getFieldName()); + assertEquals("MyFieldNamE", header.toSingleMap().keySet().toArray(new String[0])[0]); + assertEquals("MyFieldNamE", header.toMultiMap().keySet().toArray(new String[0])[0]); + + assertEquals("myValue", header.toMultiMap().get("MyFieldNamE").get(0)); + assertEquals("myValue", header.toSingleMap().get("MyFieldNamE")); + } + + @Test + public void testDeepCopy() { + Header header = new Header(1); + header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED + ";boundary=123", 1); + + Header copy = header.clone(); + assertEquals(header.getHeaders(HttpHeader.CONTENT_TYPE), copy.getHeaders(HttpHeader.CONTENT_TYPE)); + assertEquals(header.getHeader(HttpHeader.CONTENT_TYPE), copy.getHeader(HttpHeader.CONTENT_TYPE)); + assertEquals(header.getHeaderField(HttpHeader.CONTENT_TYPE), copy.getHeaderField(HttpHeader.CONTENT_TYPE)); + + assertTrue(header.getHeaders(HttpHeader.CONTENT_TYPE) != copy.getHeaders(HttpHeader.CONTENT_TYPE)); + assertTrue(header.getHeaderField(HttpHeader.CONTENT_TYPE) != copy.getHeaderField(HttpHeader.CONTENT_TYPE)); + } + + @Test + public void testMatcherNoHeader() { + Header header = new Header(1); + + assertFalse(header.isHeaderMatching(HttpHeader.CONTENT_TYPE, BatchParserCommon.PATTERN_MULTIPART_BOUNDARY)); + } + +// @Test +// public void testMatcherFail() { +// Header header = new Header(1); +// header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED + ";boundary=123", 1); +// +// assertFalse(header.isHeaderMatching(HttpHeader.CONTENT_TYPE, BatchParserCommon.PATTERN_HEADER_LINE)); +// } + + @Test + public void testDuplicatedAddList() { + Header header = new Header(1); + header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1); + header.addHeader(HttpHeader.CONTENT_TYPE, Arrays.asList(new String[] { HttpContentType.MULTIPART_MIXED, + HttpContentType.APPLICATION_ATOM_SVC }), 2); + + assertEquals(HttpContentType.MULTIPART_MIXED + ", " + HttpContentType.APPLICATION_ATOM_SVC, header + .getHeader(HttpHeader.CONTENT_TYPE)); + assertEquals(2, header.getHeaders(HttpHeader.CONTENT_TYPE).size()); + assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeaders(HttpHeader.CONTENT_TYPE).get(0)); + assertEquals(HttpContentType.APPLICATION_ATOM_SVC, header.getHeaders(HttpHeader.CONTENT_TYPE).get(1)); + } + + @Test + public void testRemove() { + Header header = new Header(1); + header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1); + header.removeHeader(HttpHeader.CONTENT_TYPE); + + assertNull(header.getHeader(HttpHeader.CONTENT_TYPE)); + assertEquals(0, header.getHeaders(HttpHeader.CONTENT_TYPE).size()); + } + + @Test + public void testMultipleValues() { + Header header = new Header(1); + header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.MULTIPART_MIXED, 1); + header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.APPLICATION_ATOM_SVC, 2); + header.addHeader(HttpHeader.CONTENT_TYPE, HttpContentType.APPLICATION_ATOM_XML, 3); + + final String fullHeaderString = + HttpContentType.MULTIPART_MIXED + ", " + HttpContentType.APPLICATION_ATOM_SVC + ", " + + HttpContentType.APPLICATION_ATOM_XML; + + assertEquals(fullHeaderString, header.getHeader(HttpHeader.CONTENT_TYPE)); + assertEquals(3, header.getHeaders(HttpHeader.CONTENT_TYPE).size()); + assertEquals(HttpContentType.MULTIPART_MIXED, header.getHeaders(HttpHeader.CONTENT_TYPE).get(0)); + assertEquals(HttpContentType.APPLICATION_ATOM_SVC, header.getHeaders(HttpHeader.CONTENT_TYPE).get(1)); + assertEquals(HttpContentType.APPLICATION_ATOM_XML, header.getHeaders(HttpHeader.CONTENT_TYPE).get(2)); + } + + @Test + public void testSplitValues() { + final String values = "abc, def,123,77, 99, ysd"; + List splittedValues = Header.splitValuesByComma(values); + + assertEquals(6, splittedValues.size()); + assertEquals("abc", splittedValues.get(0)); + assertEquals("def", splittedValues.get(1)); + assertEquals("123", splittedValues.get(2)); + assertEquals("77", splittedValues.get(3)); + assertEquals("99", splittedValues.get(4)); + assertEquals("ysd", splittedValues.get(5)); + } +} diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/writer/BatchResponseWriterTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/writer/BatchResponseWriterTest.java new file mode 100644 index 000000000..ec45a22ba --- /dev/null +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/batch/writer/BatchResponseWriterTest.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.core.batch.writer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.batch.ODataResponsePart; +import org.apache.olingo.server.core.batch.BatchException; +import org.apache.olingo.server.core.batch.StringUtil; +import org.apache.olingo.server.core.batch.parser.BatchParserCommon; +import org.apache.olingo.server.core.batch.parser.BufferedReaderIncludingLineEndings; +import org.junit.Test; + +public class BatchResponseWriterTest { + private static final String CRLF = "\r\n"; + + @Test + public void testBatchResponse() throws IOException, BatchException { + final List parts = new ArrayList(); + ODataResponse response = new ODataResponse(); + response.setStatusCode(HttpStatusCode.OK.getStatusCode()); + response.setHeader(HttpHeader.CONTENT_TYPE, "application/json"); + response.setContent(StringUtil.toInputStream("Walter Winter" + CRLF)); + + List responses = new ArrayList(1); + responses.add(response); + parts.add(new ODataResponsePartImpl(responses, false)); + + ODataResponse changeSetResponse = new ODataResponse(); + changeSetResponse.setStatusCode(HttpStatusCode.NO_CONTENT.getStatusCode()); + changeSetResponse.setHeader(BatchParserCommon.HTTP_CONTENT_ID, "1"); + responses = new ArrayList(1); + responses.add(changeSetResponse); + parts.add(new ODataResponsePartImpl(responses, true)); + + BatchResponseWriter writer = new BatchResponseWriter(); + ODataResponse batchResponse = new ODataResponse(); + writer.toODataResponse(parts, batchResponse); + + assertEquals(202, batchResponse.getStatusCode()); + assertNotNull(batchResponse.getContent()); + final BufferedReaderIncludingLineEndings reader = + new BufferedReaderIncludingLineEndings(new InputStreamReader(batchResponse.getContent())); + final List body = reader.toList(); + reader.close(); + + int line = 0; + assertEquals(25, body.size()); + assertTrue(body.get(line++).contains("--batch_")); + assertEquals("Content-Type: application/http" + CRLF, body.get(line++)); + assertEquals("Content-Transfer-Encoding: binary" + CRLF, body.get(line++)); + assertEquals(CRLF, body.get(line++)); + assertEquals("HTTP/1.1 200 OK" + CRLF, body.get(line++)); + assertEquals("Content-Type: application/json" + CRLF, body.get(line++)); + assertEquals("Content-Length: 15" + CRLF, body.get(line++)); + assertEquals(CRLF, body.get(line++)); + assertEquals("Walter Winter" + CRLF, body.get(line++)); + assertEquals(CRLF, body.get(line++)); + assertTrue(body.get(line++).contains("--batch_")); + assertTrue(body.get(line++).contains("Content-Type: multipart/mixed; boundary=changeset_")); + assertEquals(CRLF, body.get(line++)); + assertTrue(body.get(line++).contains("--changeset_")); + assertEquals("Content-Type: application/http" + CRLF, body.get(line++)); + assertEquals("Content-Transfer-Encoding: binary" + CRLF, body.get(line++)); + assertEquals("Content-Id: 1" + CRLF, body.get(line++)); + assertEquals(CRLF, body.get(line++)); + assertEquals("HTTP/1.1 204 No Content" + CRLF, body.get(line++)); + assertEquals("Content-Length: 0" + CRLF, body.get(line++)); + assertEquals(CRLF, body.get(line++)); + assertEquals(CRLF, body.get(line++)); + assertTrue(body.get(line++).contains("--changeset_")); + assertEquals(CRLF, body.get(line++)); + assertTrue(body.get(line++).contains("--batch_")); + } + + @Test + public void testResponse() throws IOException, BatchException { + List parts = new ArrayList(); + ODataResponse response = new ODataResponse(); + response.setStatusCode(HttpStatusCode.OK.getStatusCode()); + response.setHeader(HttpHeader.CONTENT_TYPE, "application/json"); + response.setContent(StringUtil.toInputStream("Walter Winter")); + + List responses = new ArrayList(1); + responses.add(response); + parts.add(new ODataResponsePartImpl(responses, false)); + + ODataResponse batchResponse = new ODataResponse(); + new BatchResponseWriter().toODataResponse(parts, batchResponse); + + assertEquals(202, batchResponse.getStatusCode()); + assertNotNull(batchResponse.getContent()); + final BufferedReaderIncludingLineEndings reader = + new BufferedReaderIncludingLineEndings(new InputStreamReader(batchResponse.getContent())); + final List body = reader.toList(); + reader.close(); + + int line = 0; + assertEquals(10, body.size()); + assertTrue(body.get(line++).contains("--batch_")); + assertEquals("Content-Type: application/http" + CRLF, body.get(line++)); + assertEquals("Content-Transfer-Encoding: binary" + CRLF, body.get(line++)); + assertEquals(CRLF, body.get(line++)); + assertEquals("HTTP/1.1 200 OK" + CRLF, body.get(line++)); + assertEquals("Content-Type: application/json" + CRLF, body.get(line++)); + assertEquals("Content-Length: 13" + CRLF, body.get(line++)); + assertEquals(CRLF, body.get(line++)); + assertEquals("Walter Winter" + CRLF, body.get(line++)); + assertTrue(body.get(line++).contains("--batch_")); + } + + @Test + public void testChangeSetResponse() throws IOException, BatchException { + List parts = new ArrayList(); + ODataResponse response = new ODataResponse(); + response.setHeader(BatchParserCommon.HTTP_CONTENT_ID, "1"); + response.setStatusCode(HttpStatusCode.NO_CONTENT.getStatusCode()); + + List responses = new ArrayList(1); + responses.add(response); + parts.add(new ODataResponsePartImpl(responses, true)); + + BatchResponseWriter writer = new BatchResponseWriter(); + ODataResponse batchResponse = new ODataResponse(); + writer.toODataResponse(parts, batchResponse); + + assertEquals(202, batchResponse.getStatusCode()); + assertNotNull(batchResponse.getContent()); + + final BufferedReaderIncludingLineEndings reader = + new BufferedReaderIncludingLineEndings(new InputStreamReader(batchResponse.getContent())); + final List body = reader.toList(); + reader.close(); + + int line = 0; + assertEquals(15, body.size()); + assertTrue(body.get(line++).contains("--batch_")); + assertTrue(body.get(line++).contains("Content-Type: multipart/mixed; boundary=changeset_")); + assertEquals(CRLF, body.get(line++)); + assertTrue(body.get(line++).contains("--changeset_")); + assertEquals("Content-Type: application/http" + CRLF, body.get(line++)); + assertEquals("Content-Transfer-Encoding: binary" + CRLF, body.get(line++)); + assertEquals("Content-Id: 1" + CRLF, body.get(line++)); + assertEquals(CRLF, body.get(line++)); + assertEquals("HTTP/1.1 204 No Content" + CRLF, body.get(line++)); + assertEquals("Content-Length: 0" + CRLF, body.get(line++)); + assertEquals(CRLF, body.get(line++)); + assertEquals(CRLF, body.get(line++)); + assertTrue(body.get(line++).contains("--changeset_")); + assertEquals(CRLF, body.get(line++)); + assertTrue(body.get(line++).contains("--batch_")); + } +} diff --git a/lib/server-core/src/test/resources/batchLarge.batch b/lib/server-core/src/test/resources/batchLarge.batch new file mode 100644 index 000000000..faadea1cc --- /dev/null +++ b/lib/server-core/src/test/resources/batchLarge.batch @@ -0,0 +1,2422 @@ +--batch_8194-cf13-1f56 +Content-Type: application/http +Content-Transfer-Encoding: binary + +GET Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Accept: application/atom+xml +MaxDataServiceVersion: 2.0 +DataServiceVersion: 2.0 +Content-Type: application/atom+xml +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + +--batch_8194-cf13-1f56 +Content-Type: application/http +Content-Transfer-Encoding: binary + +GET Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Accept: application/atom+xml +MaxDataServiceVersion: 2.0 +DataServiceVersion: 2.0 +Content-Type: application/atom+xml +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + +--batch_8194-cf13-1f56 +Content-Type: application/http +Content-Transfer-Encoding: binary + +GET Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Accept: application/atom+xml +MaxDataServiceVersion: 2.0 +DataServiceVersion: 2.0 +Content-Type: application/atom+xml +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + +--batch_8194-cf13-1f56 +Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +POST Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Rooms('2') + + Room 2 + 2013-04-03T10:53:26.021+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees + + Employees + 2013-04-03T10:53:26.024+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('2') + + Frederic Fall + 2003-07-01T00:00:00Z + + + + + + + + + 2 + Frederic Fall + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 32 + 2003-07-01T00:00:00 + Employees('2')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('3') + + Jonathan Smith + 2013-04-03T10:53:26.025+02:00 + + + + + + + + + 3 + Jonathan Smith + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 56 + + Employees('3')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('4') + + Peter Burke + 2004-09-12T00:00:00Z + + + + + + + + + 4 + Peter Burke + 3 + 2 + 2 + + + 69190 + Walldorf + + Germany + + 39 + 2004-09-12T00:00:00 + Employees('4')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('6') + + Susan Bay + 2010-12-01T00:00:00Z + + + + + + + + + 6 + Susan Bay + 1 + 2 + 3 + + + 69190 + Walldorf + + Germany + + 29 + 2010-12-01T00:00:00 + Employees('6')/$value + + + + + + + + + 2 + Room 2 + 5 + 2 + + + + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +POST Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Rooms('2') + + Room 2 + 2013-04-03T10:53:26.021+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees + + Employees + 2013-04-03T10:53:26.024+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('2') + + Frederic Fall + 2003-07-01T00:00:00Z + + + + + + + + + 2 + Frederic Fall + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 32 + 2003-07-01T00:00:00 + Employees('2')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('3') + + Jonathan Smith + 2013-04-03T10:53:26.025+02:00 + + + + + + + + + 3 + Jonathan Smith + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 56 + + Employees('3')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('4') + + Peter Burke + 2004-09-12T00:00:00Z + + + + + + + + + 4 + Peter Burke + 3 + 2 + 2 + + + 69190 + Walldorf + + Germany + + 39 + 2004-09-12T00:00:00 + Employees('4')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('6') + + Susan Bay + 2010-12-01T00:00:00Z + + + + + + + + + 6 + Susan Bay + 1 + 2 + 3 + + + 69190 + Walldorf + + Germany + + 29 + 2010-12-01T00:00:00 + Employees('6')/$value + + + + + + + + + 2 + Room 2 + 5 + 2 + + + + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +PUT Employees('1') HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive + +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:19000/abc/EntryXmlChangeTest/Employees('9') + Mister X + Z + + + + 1 + Mister X + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + + Employees('1')/$value + + +--changeset_f980-1cb6-94dd-- +--batch_8194-cf13-1f56 +Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +POST Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Rooms('2') + + Room 2 + 2013-04-03T10:53:26.021+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees + + Employees + 2013-04-03T10:53:26.024+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('2') + + Frederic Fall + 2003-07-01T00:00:00Z + + + + + + + + + 2 + Frederic Fall + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 32 + 2003-07-01T00:00:00 + Employees('2')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('3') + + Jonathan Smith + 2013-04-03T10:53:26.025+02:00 + + + + + + + + + 3 + Jonathan Smith + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 56 + + Employees('3')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('4') + + Peter Burke + 2004-09-12T00:00:00Z + + + + + + + + + 4 + Peter Burke + 3 + 2 + 2 + + + 69190 + Walldorf + + Germany + + 39 + 2004-09-12T00:00:00 + Employees('4')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('6') + + Susan Bay + 2010-12-01T00:00:00Z + + + + + + + + + 6 + Susan Bay + 1 + 2 + 3 + + + 69190 + Walldorf + + Germany + + 29 + 2010-12-01T00:00:00 + Employees('6')/$value + + + + + + + + + 2 + Room 2 + 5 + 2 + + + + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +POST Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Rooms('2') + + Room 2 + 2013-04-03T10:53:26.021+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees + + Employees + 2013-04-03T10:53:26.024+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('2') + + Frederic Fall + 2003-07-01T00:00:00Z + + + + + + + + + 2 + Frederic Fall + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 32 + 2003-07-01T00:00:00 + Employees('2')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('3') + + Jonathan Smith + 2013-04-03T10:53:26.025+02:00 + + + + + + + + + 3 + Jonathan Smith + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 56 + + Employees('3')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('4') + + Peter Burke + 2004-09-12T00:00:00Z + + + + + + + + + 4 + Peter Burke + 3 + 2 + 2 + + + 69190 + Walldorf + + Germany + + 39 + 2004-09-12T00:00:00 + Employees('4')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('6') + + Susan Bay + 2010-12-01T00:00:00Z + + + + + + + + + 6 + Susan Bay + 1 + 2 + 3 + + + 69190 + Walldorf + + Germany + + 29 + 2010-12-01T00:00:00 + Employees('6')/$value + + + + + + + + + 2 + Room 2 + 5 + 2 + + + + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +PUT Employees('1') HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:19000/abc/EntryXmlChangeTest/Employees('9') + Mister X + Z + + + + 1 + Mister X + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + + Employees('1')/$value + + +--changeset_f980-1cb6-94dd-- +--batch_8194-cf13-1f56 +Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +POST Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Rooms('2') + + Room 2 + 2013-04-03T10:53:26.021+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees + + Employees + 2013-04-03T10:53:26.024+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('2') + + Frederic Fall + 2003-07-01T00:00:00Z + + + + + + + + + 2 + Frederic Fall + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 32 + 2003-07-01T00:00:00 + Employees('2')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('3') + + Jonathan Smith + 2013-04-03T10:53:26.025+02:00 + + + + + + + + + 3 + Jonathan Smith + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 56 + + Employees('3')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('4') + + Peter Burke + 2004-09-12T00:00:00Z + + + + + + + + + 4 + Peter Burke + 3 + 2 + 2 + + + 69190 + Walldorf + + Germany + + 39 + 2004-09-12T00:00:00 + Employees('4')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('6') + + Susan Bay + 2010-12-01T00:00:00Z + + + + + + + + + 6 + Susan Bay + 1 + 2 + 3 + + + 69190 + Walldorf + + Germany + + 29 + 2010-12-01T00:00:00 + Employees('6')/$value + + + + + + + + + 2 + Room 2 + 5 + 2 + + + + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +POST Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Rooms('2') + + Room 2 + 2013-04-03T10:53:26.021+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees + + Employees + 2013-04-03T10:53:26.024+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('2') + + Frederic Fall + 2003-07-01T00:00:00Z + + + + + + + + + 2 + Frederic Fall + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 32 + 2003-07-01T00:00:00 + Employees('2')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('3') + + Jonathan Smith + 2013-04-03T10:53:26.025+02:00 + + + + + + + + + 3 + Jonathan Smith + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 56 + + Employees('3')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('4') + + Peter Burke + 2004-09-12T00:00:00Z + + + + + + + + + 4 + Peter Burke + 3 + 2 + 2 + + + 69190 + Walldorf + + Germany + + 39 + 2004-09-12T00:00:00 + Employees('4')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('6') + + Susan Bay + 2010-12-01T00:00:00Z + + + + + + + + + 6 + Susan Bay + 1 + 2 + 3 + + + 69190 + Walldorf + + Germany + + 29 + 2010-12-01T00:00:00 + Employees('6')/$value + + + + + + + + + 2 + Room 2 + 5 + 2 + + + + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +PUT Employees('1') HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:19000/abc/EntryXmlChangeTest/Employees('9') + Mister X + Z + + + + 1 + Mister X + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + + Employees('1')/$value + + +--changeset_f980-1cb6-94dd-- +--batch_8194-cf13-1f56 +Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +POST Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Rooms('2') + + Room 2 + 2013-04-03T10:53:26.021+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees + + Employees + 2013-04-03T10:53:26.024+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('2') + + Frederic Fall + 2003-07-01T00:00:00Z + + + + + + + + + 2 + Frederic Fall + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 32 + 2003-07-01T00:00:00 + Employees('2')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('3') + + Jonathan Smith + 2013-04-03T10:53:26.025+02:00 + + + + + + + + + 3 + Jonathan Smith + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 56 + + Employees('3')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('4') + + Peter Burke + 2004-09-12T00:00:00Z + + + + + + + + + 4 + Peter Burke + 3 + 2 + 2 + + + 69190 + Walldorf + + Germany + + 39 + 2004-09-12T00:00:00 + Employees('4')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('6') + + Susan Bay + 2010-12-01T00:00:00Z + + + + + + + + + 6 + Susan Bay + 1 + 2 + 3 + + + 69190 + Walldorf + + Germany + + 29 + 2010-12-01T00:00:00 + Employees('6')/$value + + + + + + + + + 2 + Room 2 + 5 + 2 + + + + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +POST Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Rooms('2') + + Room 2 + 2013-04-03T10:53:26.021+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees + + Employees + 2013-04-03T10:53:26.024+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('2') + + Frederic Fall + 2003-07-01T00:00:00Z + + + + + + + + + 2 + Frederic Fall + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 32 + 2003-07-01T00:00:00 + Employees('2')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('3') + + Jonathan Smith + 2013-04-03T10:53:26.025+02:00 + + + + + + + + + 3 + Jonathan Smith + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 56 + + Employees('3')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('4') + + Peter Burke + 2004-09-12T00:00:00Z + + + + + + + + + 4 + Peter Burke + 3 + 2 + 2 + + + 69190 + Walldorf + + Germany + + 39 + 2004-09-12T00:00:00 + Employees('4')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('6') + + Susan Bay + 2010-12-01T00:00:00Z + + + + + + + + + 6 + Susan Bay + 1 + 2 + 3 + + + 69190 + Walldorf + + Germany + + 29 + 2010-12-01T00:00:00 + Employees('6')/$value + + + + + + + + + 2 + Room 2 + 5 + 2 + + + + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +PUT Employees('1') HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:19000/abc/EntryXmlChangeTest/Employees('9') + Mister X + Z + + + + 1 + Mister X + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + + Employees('1')/$value + + +--changeset_f980-1cb6-94dd-- +--batch_8194-cf13-1f56 +Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +POST Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Rooms('2') + + Room 2 + 2013-04-03T10:53:26.021+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees + + Employees + 2013-04-03T10:53:26.024+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('2') + + Frederic Fall + 2003-07-01T00:00:00Z + + + + + + + + + 2 + Frederic Fall + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 32 + 2003-07-01T00:00:00 + Employees('2')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('3') + + Jonathan Smith + 2013-04-03T10:53:26.025+02:00 + + + + + + + + + 3 + Jonathan Smith + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 56 + + Employees('3')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('4') + + Peter Burke + 2004-09-12T00:00:00Z + + + + + + + + + 4 + Peter Burke + 3 + 2 + 2 + + + 69190 + Walldorf + + Germany + + 39 + 2004-09-12T00:00:00 + Employees('4')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('6') + + Susan Bay + 2010-12-01T00:00:00Z + + + + + + + + + 6 + Susan Bay + 1 + 2 + 3 + + + 69190 + Walldorf + + Germany + + 29 + 2010-12-01T00:00:00 + Employees('6')/$value + + + + + + + + + 2 + Room 2 + 5 + 2 + + + + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +POST Employees HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Rooms('2') + + Room 2 + 2013-04-03T10:53:26.021+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees + + Employees + 2013-04-03T10:53:26.024+02:00 + + + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('2') + + Frederic Fall + 2003-07-01T00:00:00Z + + + + + + + + + 2 + Frederic Fall + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 32 + 2003-07-01T00:00:00 + Employees('2')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('3') + + Jonathan Smith + 2013-04-03T10:53:26.025+02:00 + + + + + + + + + 3 + Jonathan Smith + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + 56 + + Employees('3')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('4') + + Peter Burke + 2004-09-12T00:00:00Z + + + + + + + + + 4 + Peter Burke + 3 + 2 + 2 + + + 69190 + Walldorf + + Germany + + 39 + 2004-09-12T00:00:00 + Employees('4')/$value + + + + + http://localhost:8080/org.apache.olingo.odata2.ref.web/ReferenceScenario.svc/Employees('6') + + Susan Bay + 2010-12-01T00:00:00Z + + + + + + + + + 6 + Susan Bay + 1 + 2 + 3 + + + 69190 + Walldorf + + Germany + + 29 + 2010-12-01T00:00:00 + Employees('6')/$value + + + + + + + + + 2 + Room 2 + 5 + 2 + + + + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: 1 + +PUT Employees('1') HTTP/1.1 +Host: http://localhost/odata +Connection: keep-alive +Content-Type: application/atom+xml +Accept: */* +Accept-Encoding: gzip,deflate +Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 + + + + http://localhost:19000/abc/EntryXmlChangeTest/Employees('9') + Mister X + Z + + + + 1 + Mister X + 1 + 2 + 1 + + + 69190 + Walldorf + + Germany + + + Employees('1')/$value + + +--changeset_f980-1cb6-94dd-- +--batch_8194-cf13-1f56-- \ No newline at end of file diff --git a/lib/server-core/src/test/resources/batchWithContent.batch b/lib/server-core/src/test/resources/batchWithContent.batch new file mode 100644 index 000000000..834857d4b --- /dev/null +++ b/lib/server-core/src/test/resources/batchWithContent.batch @@ -0,0 +1 @@ +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REQEREBERAREQEREBERAREQEREBERAREQEREBERB//9k= \ No newline at end of file diff --git a/lib/server-core/src/test/resources/batchWithPost.batch b/lib/server-core/src/test/resources/batchWithPost.batch new file mode 100644 index 000000000..b7038e915 --- /dev/null +++ b/lib/server-core/src/test/resources/batchWithPost.batch @@ -0,0 +1,39 @@ +--batch_8194-cf13-1f56 +Content-Type: application/http +Content-Transfer-Encoding: binary + +GET http://localhost/odata/Employees('2')/EmployeeName?$format=json HTTP/1.1 +Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1 +Accept-Language:en-US,en;q=0.7,en-UK;q=0.9 +MaxDataServiceVersion: 2.0 + + +--batch_8194-cf13-1f56 +Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd + +--changeset_f980-1cb6-94dd +Content-Type: application/http +Content-Transfer-Encoding: binary +Content-ID: changeRequest1 + +PUT Employees('2')/EmployeeName HTTP/1.1 +Content-Length: 100000 +Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1 +DataServiceVersion: 1.0 +Content-Type: application/json;odata=verbose +MaxDataServiceVersion: 2.0 + +{"EmployeeName":"Frederic Fall MODIFIED"} + +--changeset_f980-1cb6-94dd-- + +--batch_8194-cf13-1f56 +Content-Type: application/http +Content-Transfer-Encoding: binary + +GET Employees('2')/EmployeeName?$format=json HTTP/1.1 +Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1 +MaxDataServiceVersion: 2.0 + + +--batch_8194-cf13-1f56--