CAMEL-11056: Create new camel-olingo4 component for supporting OData 4.0 Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/e6eded4c Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/e6eded4c Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/e6eded4c
Branch: refs/heads/master Commit: e6eded4c217b1734f36731396bff2f7fb9ee6f04 Parents: 46180b9 Author: Dmitry Volodin <dmvo...@gmail.com> Authored: Thu Mar 23 15:48:32 2017 +0300 Committer: Andrea Cosentino <anco...@gmail.com> Committed: Fri Mar 24 09:49:39 2017 +0100 ---------------------------------------------------------------------- .../camel-olingo4/camel-olingo4-api/pom.xml | 141 ++++ .../camel/component/olingo4/api/Olingo4App.java | 145 ++++ .../olingo4/api/Olingo4ResponseHandler.java | 43 + .../api/batch/Olingo4BatchChangeRequest.java | 103 +++ .../api/batch/Olingo4BatchQueryRequest.java | 76 ++ .../olingo4/api/batch/Olingo4BatchRequest.java | 44 + .../olingo4/api/batch/Olingo4BatchResponse.java | 63 ++ .../component/olingo4/api/batch/Operation.java | 39 + .../api/impl/AbstractFutureCallback.java | 111 +++ .../olingo4/api/impl/Olingo4AppImpl.java | 841 +++++++++++++++++++ .../src/main/resources/META-INF/LICENSE.txt | 203 +++++ .../src/main/resources/META-INF/NOTICE.txt | 11 + .../component/olingo4/Olingo4AppAPITest.java | 543 ++++++++++++ .../src/test/resources/log4j2.properties | 28 + .../camel-olingo4-component/pom.xml | 191 +++++ .../src/main/docs/olingo4-component.adoc | 252 ++++++ .../src/main/java/META-INF/MANIFEST.MF | 3 + .../component/olingo4/Olingo4AppWrapper.java | 108 +++ .../component/olingo4/Olingo4Component.java | 184 ++++ .../component/olingo4/Olingo4Configuration.java | 221 +++++ .../component/olingo4/Olingo4Consumer.java | 92 ++ .../component/olingo4/Olingo4Endpoint.java | 222 +++++ .../component/olingo4/Olingo4Producer.java | 106 +++ .../olingo4/internal/Olingo4Constants.java | 29 + .../internal/Olingo4PropertiesHelper.java | 39 + .../src/main/resources/META-INF/LICENSE.txt | 203 +++++ .../src/main/resources/META-INF/NOTICE.txt | 11 + .../services/org/apache/camel/component/olingo4 | 18 + .../src/signatures/olingo-api-signature.txt | 8 + .../olingo4/AbstractOlingo4TestSupport.java | 104 +++ .../component/olingo4/Olingo4ComponentTest.java | 264 ++++++ .../src/test/resources/log4j2.properties | 28 + components/camel-olingo4/pom.xml | 38 + components/pom.xml | 1 + parent/pom.xml | 1 + .../camel-olingo4-starter/pom.xml | 67 ++ .../Olingo4ComponentAutoConfiguration.java | 111 +++ .../Olingo4ComponentConfiguration.java | 201 +++++ .../src/main/resources/META-INF/LICENSE.txt | 203 +++++ .../src/main/resources/META-INF/NOTICE.txt | 11 + ...dditional-spring-configuration-metadata.json | 10 + .../main/resources/META-INF/spring.factories | 19 + .../src/main/resources/META-INF/spring.provides | 18 + .../spring-boot/components-starter/pom.xml | 1 + 44 files changed, 5155 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/e6eded4c/components/camel-olingo4/camel-olingo4-api/pom.xml ---------------------------------------------------------------------- diff --git a/components/camel-olingo4/camel-olingo4-api/pom.xml b/components/camel-olingo4/camel-olingo4-api/pom.xml new file mode 100644 index 0000000..7f0495c --- /dev/null +++ b/components/camel-olingo4/camel-olingo4-api/pom.xml @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.camel</groupId> + <artifactId>camel-olingo4-parent</artifactId> + <version>2.19.0-SNAPSHOT</version> + </parent> + + <artifactId>camel-olingo4-api</artifactId> + <name>Camel :: Olingo4 :: API</name> + <description>Camel Olingo4 API</description> + <packaging>jar</packaging> + + <properties> + <camel.osgi.export.pkg>org.apache.camel.component.olingo4.api*</camel.osgi.export.pkg> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.olingo</groupId> + <artifactId>odata-commons-api</artifactId> + <version>${olingo4-version}</version> + </dependency> + <dependency> + <groupId>org.apache.olingo</groupId> + <artifactId>odata-commons-core</artifactId> + <version>${olingo4-version}</version> + </dependency> + <dependency> + <groupId>org.apache.olingo</groupId> + <artifactId>odata-client-core</artifactId> + <version>${olingo4-version}</version> + </dependency> + <dependency> + <groupId>org.apache.olingo</groupId> + <artifactId>odata-server-core</artifactId> + <version>${olingo4-version}</version> + </dependency> + + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpasyncclient</artifactId> + </dependency> + + <dependency> + <groupId>net.sf.jsignature.io-tools</groupId> + <artifactId>easystream</artifactId> + <version>1.2.15</version> + </dependency> + + <!-- logging --> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-api</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-slf4j-impl</artifactId> + <scope>test</scope> + </dependency> + + <!-- testing --> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-test</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <defaultGoal>install</defaultGoal> + + <plugins> + + <!-- to generate API Javadoc --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <executions> + <execution> + <id>add-javadoc</id> + <goals> + <goal>jar</goal> + </goals> + <configuration> + <attach>true</attach> + <source>${jdk.version}</source> + <quiet>true</quiet> + <detectOfflineLinks>false</detectOfflineLinks> + <javadocVersion>${jdk.version}</javadocVersion> + <encoding>UTF-8</encoding> + </configuration> + </execution> + </executions> + </plugin> + + </plugins> + </build> + + <!-- Disable Java 8 doclint checks to avoid Javadoc plugin failures --> + <profiles> + <profile> + <id>doclint-java8-disable</id> + <activation> + <jdk>[1.8,</jdk> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <configuration> + <additionalparam>-Xdoclint:none</additionalparam> + </configuration> + </plugin> + </plugins> + </build> + </profile> + </profiles> + +</project> http://git-wip-us.apache.org/repos/asf/camel/blob/e6eded4c/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/Olingo4App.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/Olingo4App.java b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/Olingo4App.java new file mode 100644 index 0000000..95d42c2 --- /dev/null +++ b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/Olingo4App.java @@ -0,0 +1,145 @@ +/** + * 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.camel.component.olingo4.api; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +import org.apache.camel.component.olingo4.api.batch.Olingo4BatchResponse; +import org.apache.olingo.commons.api.edm.Edm; +import org.apache.olingo.commons.api.http.HttpStatusCode; + +/** + * Olingo4 Client Api Interface. + */ +public interface Olingo4App { + + /** + * Sets Service base URI. + * @param serviceUri + */ + void setServiceUri(String serviceUri); + + /** + * Returns Service base URI. + * @return service base URI. + */ + String getServiceUri(); + + /** + * Sets custom Http headers to add to every service request. + * @param httpHeaders custom Http headers. + */ + void setHttpHeaders(Map<String, String> httpHeaders); + + /** + * Returns custom Http headers. + * @return custom Http headers. + */ + Map<String, String> getHttpHeaders(); + + /** + * Returns content type for service calls. Defaults to <code>application/json;charset=utf-8</code>. + * @return content type. + */ + String getContentType(); + + /** + * Set default service call content type. + * @param contentType content type. + */ + void setContentType(String contentType); + + /** + * Closes resources. + */ + void close(); + + /** + * Reads an OData resource and invokes callback with appropriate result. + * @param edm Service Edm, read from calling <code>read(null, "$metdata", null, responseHandler)</code> + * @param resourcePath OData Resource path + * @param queryParams OData query params + * http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part1-protocol.html#_Toc453752288 + * @param responseHandler callback handler + */ + <T> void read(Edm edm, String resourcePath, Map<String, String> queryParams, Olingo4ResponseHandler<T> responseHandler); + + /** + * Reads an OData resource and invokes callback with the unparsed input stream. + * @param edm Service Edm, read from calling <code>read(null, "$metdata", null, responseHandler)</code> + * @param resourcePath OData Resource path + * @param queryParams OData query params + * http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part1-protocol.html#_Toc453752288 + * @param responseHandler callback handler + */ + void uread(Edm edm, String resourcePath, Map<String, String> queryParams, + Olingo4ResponseHandler<InputStream> responseHandler); + + /** + * Deletes an OData resource and invokes callback + * with {@link org.apache.olingo.commons.api.http.HttpStatusCode} on success, or with exception on failure. + * @param resourcePath resource path for Entry + * @param responseHandler {@link org.apache.olingo.commons.api.http.HttpStatusCode} callback handler + */ + void delete(String resourcePath, Olingo4ResponseHandler<HttpStatusCode> responseHandler); + + /** + * Creates a new OData resource. + * @param edm service Edm + * @param resourcePath resource path to create + * @param data request data + * @param responseHandler callback handler + */ + <T> void create(Edm edm, String resourcePath, Object data, Olingo4ResponseHandler<T> responseHandler); + + /** + * Updates an OData resource. + * @param edm service Edm + * @param resourcePath resource path to update + * @param data updated data + * @param responseHandler {@link org.apache.olingo.client.api.domain.ClientEntity} callback handler + */ + <T> void update(Edm edm, String resourcePath, Object data, Olingo4ResponseHandler<T> responseHandler); + + /** + * Patches/merges an OData resource using HTTP PATCH. + * @param edm service Edm + * @param resourcePath resource path to update + * @param data patch/merge data + * @param responseHandler {@link org.apache.olingo.client.api.domain.ClientEntity} callback handler + */ + <T> void patch(Edm edm, String resourcePath, Object data, Olingo4ResponseHandler<T> responseHandler); + + /** + * Patches/merges an OData resource using HTTP MERGE. + * @param edm service Edm + * @param resourcePath resource path to update + * @param data patch/merge data + * @param responseHandler {@link org.apache.olingo.client.api.domain.ClientEntity} callback handler + */ + <T> void merge(Edm edm, String resourcePath, Object data, Olingo4ResponseHandler<T> responseHandler); + + /** + * Executes a batch request. + * @param edm service Edm + * @param data ordered {@link org.apache.camel.component.olingo4.api.batch.Olingo4BatchRequest} list + * @param responseHandler callback handler + */ + void batch(Edm edm, Object data, Olingo4ResponseHandler<List<Olingo4BatchResponse>> responseHandler); +} http://git-wip-us.apache.org/repos/asf/camel/blob/e6eded4c/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/Olingo4ResponseHandler.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/Olingo4ResponseHandler.java b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/Olingo4ResponseHandler.java new file mode 100644 index 0000000..13438ba --- /dev/null +++ b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/Olingo4ResponseHandler.java @@ -0,0 +1,43 @@ +/** + * 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.camel.component.olingo4.api; + +/** + * Callback interface to asynchronously process Olingo4 response. + */ +public interface Olingo4ResponseHandler<T> { + + /** + * Handle response data on successful completion of Olingo4 request. + * @param response response data from Olingo4, may be NULL for Olingo4 operations with no response data. + */ + void onResponse(T response); + + /** + * Handle exception raised from Olingo4 request. + * @param ex exception from Olingo4 request. + * May be an instance of {@link org.apache.olingo.commons.api.ex.ODataException} or + * some other exception, such as {@link java.io.IOException} + */ + void onException(Exception ex); + + /** + * Handle Olingo4 request cancellation. + * May be caused by the underlying HTTP connection being shutdown asynchronously. + */ + void onCanceled(); +} http://git-wip-us.apache.org/repos/asf/camel/blob/e6eded4c/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchChangeRequest.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchChangeRequest.java b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchChangeRequest.java new file mode 100644 index 0000000..5b4e1eb --- /dev/null +++ b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchChangeRequest.java @@ -0,0 +1,103 @@ +/** + * 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.camel.component.olingo4.api.batch; + +import java.util.Map; + +/** + * Batch Change part. + */ +public class Olingo4BatchChangeRequest extends Olingo4BatchRequest { + + protected String contentId; + protected Operation operation; + protected Object body; + + public Operation getOperation() { + return operation; + } + + public Object getBody() { + return body; + } + + public String getContentId() { + return contentId; + } + + @Override + public String toString() { + return new StringBuilder("Batch Change Request{ ").append(resourceUri).append("/").append(resourcePath).append(", headers=").append(headers).append(", contentId=") + .append(contentId).append(", operation=").append(operation).append(", body=").append(body).append('}').toString(); + } + + public static Olingo4BatchChangeRequestBuilder resourcePath(String resourcePath) { + if (resourcePath == null) { + throw new IllegalArgumentException("resourcePath"); + } + return new Olingo4BatchChangeRequestBuilder().resourcePath(resourcePath); + } + + public static class Olingo4BatchChangeRequestBuilder { + + private Olingo4BatchChangeRequest request = new Olingo4BatchChangeRequest(); + + public Olingo4BatchChangeRequestBuilder resourcePath(String resourcePath) { + request.resourcePath = resourcePath; + return this; + } + + public Olingo4BatchChangeRequestBuilder resourceUri(String resourceUri) { + request.resourceUri = resourceUri; + return this; + } + + public Olingo4BatchChangeRequestBuilder headers(Map<String, String> headers) { + request.headers = headers; + return this; + } + + public Olingo4BatchChangeRequestBuilder contentId(String contentId) { + request.contentId = contentId; + return this; + } + + public Olingo4BatchChangeRequestBuilder operation(Operation operation) { + request.operation = operation; + return this; + } + + public Olingo4BatchChangeRequestBuilder body(Object body) { + request.body = body; + return this; + } + + public Olingo4BatchChangeRequest build() { + // avoid later NPEs + if (request.resourcePath == null) { + throw new IllegalArgumentException("Null resourcePath"); + } + if (request.operation == null) { + throw new IllegalArgumentException("Null operation"); + } + if (request.operation != Operation.DELETE && request.body == null) { + throw new IllegalArgumentException("Null body"); + } + return request; + } + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/e6eded4c/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchQueryRequest.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchQueryRequest.java b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchQueryRequest.java new file mode 100644 index 0000000..127a3ef --- /dev/null +++ b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchQueryRequest.java @@ -0,0 +1,76 @@ +/** + * 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.camel.component.olingo4.api.batch; + +import java.util.Map; + +/** + * Batch Query part. + */ +public class Olingo4BatchQueryRequest extends Olingo4BatchRequest { + + private Map<String, String> queryParams; + + public Map<String, String> getQueryParams() { + return queryParams; + } + + public static Olingo4BatchQueryRequestBuilder resourcePath(String resourcePath) { + if (resourcePath == null) { + throw new IllegalArgumentException("resourcePath"); + } + return new Olingo4BatchQueryRequestBuilder().resourcePath(resourcePath); + } + + @Override + public String toString() { + return new StringBuilder("Batch Query Request{ ").append(resourceUri).append("/").append(resourcePath).append(", headers=").append(headers).append(", queryParams=") + .append(queryParams).append('}').toString(); + } + + public static class Olingo4BatchQueryRequestBuilder { + private Olingo4BatchQueryRequest request = new Olingo4BatchQueryRequest(); + + public Olingo4BatchQueryRequest build() { + // avoid later NPEs + if (request.resourcePath == null) { + throw new IllegalArgumentException("Null resourcePath"); + } + return request; + } + + public Olingo4BatchQueryRequestBuilder resourceUri(String resourceUri) { + request.resourceUri = resourceUri; + return this; + } + + public Olingo4BatchQueryRequestBuilder resourcePath(String resourcePath) { + request.resourcePath = resourcePath; + return this; + } + + public Olingo4BatchQueryRequestBuilder headers(Map<String, String> headers) { + request.headers = headers; + return this; + } + + public Olingo4BatchQueryRequestBuilder queryParams(Map<String, String> queryParams) { + request.queryParams = queryParams; + return this; + } + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/e6eded4c/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchRequest.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchRequest.java b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchRequest.java new file mode 100644 index 0000000..8ad9568 --- /dev/null +++ b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchRequest.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.camel.component.olingo4.api.batch; + +import java.util.HashMap; +import java.util.Map; + +/** + * Base part in a multipart Batch request. + */ +public abstract class Olingo4BatchRequest { + + protected String resourceUri; + protected String resourcePath; + protected Map<String, String> headers = new HashMap<String, String>(); + + public String getResourceUri() { + return resourceUri; + } + + public String getResourcePath() { + return resourcePath; + } + + public Map<String, String> getHeaders() { + return headers; + } + + public abstract String toString(); +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/camel/blob/e6eded4c/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchResponse.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchResponse.java b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchResponse.java new file mode 100644 index 0000000..9d4ce91 --- /dev/null +++ b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Olingo4BatchResponse.java @@ -0,0 +1,63 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.olingo4.api.batch; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Batch Response part. + */ +public class Olingo4BatchResponse { + + private final int statusCode; + private final String statusInfo; + + private final String contentId; + + private final Map<String, String> headers; + private final Object body; + + public Olingo4BatchResponse(int statusCode, String statusInfo, String contentId, Map<String, String> headers, Object body) { + this.statusCode = statusCode; + this.statusInfo = statusInfo; + this.contentId = contentId; + this.headers = Collections.unmodifiableMap(new HashMap<String, String>(headers)); + this.body = body; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatusInfo() { + return statusInfo; + } + + public String getContentId() { + return contentId; + } + + public Map<String, String> getHeaders() { + return headers; + } + + public Object getBody() { + return body; + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/e6eded4c/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Operation.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Operation.java b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Operation.java new file mode 100644 index 0000000..5785cae --- /dev/null +++ b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/batch/Operation.java @@ -0,0 +1,39 @@ +/** + * 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.camel.component.olingo4.api.batch; + +/** +* OData operation used by {@link org.apache.camel.component.olingo4.api.batch.Olingo4BatchChangeRequest}. +*/ +public enum Operation { + + CREATE("POST"), + UPDATE("PUT"), + PATCH("PATCH"), + MERGE("MERGE"), + DELETE("DELETE"); + + private final String httpMethod; + + Operation(String httpMethod) { + this.httpMethod = httpMethod; + } + + public String getHttpMethod() { + return httpMethod; + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/e6eded4c/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/impl/AbstractFutureCallback.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/impl/AbstractFutureCallback.java b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/impl/AbstractFutureCallback.java new file mode 100644 index 0000000..1a206fa --- /dev/null +++ b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/impl/AbstractFutureCallback.java @@ -0,0 +1,111 @@ +/** + * 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.camel.component.olingo4.api.impl; + +import java.io.Closeable; +import java.io.IOException; +import java.io.StringWriter; +import java.util.regex.Pattern; + +import org.apache.camel.component.olingo4.api.Olingo4ResponseHandler; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.concurrent.FutureCallback; +import org.apache.olingo.client.api.ODataClient; +import org.apache.olingo.client.api.communication.ODataClientErrorException; +import org.apache.olingo.client.api.serialization.ODataReader; +import org.apache.olingo.client.core.ODataClientFactory; +import org.apache.olingo.client.core.serialization.ODataReaderImpl; +import org.apache.olingo.commons.api.ex.ODataError; +import org.apache.olingo.commons.api.ex.ODataException; +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.commons.api.http.HttpStatusCode; + +/** +* Helper implementation of {@link org.apache.http.concurrent.FutureCallback} + * for {@link org.apache.camel.component.olingo4.api.impl.Olingo4AppImpl} +*/ +public abstract class AbstractFutureCallback<T> implements FutureCallback<HttpResponse> { + + public static final Pattern ODATA_MIME_TYPE_PATTERN = Pattern.compile("application/((atom)|(json)|(xml)).*"); + public static final int NETWORK_CONNECT_TIMEOUT_ERROR = 599; + + private final Olingo4ResponseHandler<T> responseHandler; + + AbstractFutureCallback(Olingo4ResponseHandler<T> responseHandler) { + this.responseHandler = responseHandler; + } + + public static HttpStatusCode checkStatus(HttpResponse response) throws ODataException, ODataClientErrorException { + final StatusLine statusLine = response.getStatusLine(); + HttpStatusCode httpStatusCode = HttpStatusCode.fromStatusCode(statusLine.getStatusCode()); + if (HttpStatusCode.BAD_REQUEST.getStatusCode() <= httpStatusCode.getStatusCode() && httpStatusCode.getStatusCode() <= NETWORK_CONNECT_TIMEOUT_ERROR) { + if (response.getEntity() != null) { + try { + final ContentType responseContentType = ContentType.parse( + response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()); + + if (ODATA_MIME_TYPE_PATTERN.matcher(responseContentType.toContentTypeString()).matches()) { + final ODataReader reader = ODataClientFactory.getClient().getReader(); + final ODataError error = reader.readError(response.getEntity().getContent(), responseContentType); + + throw new ODataClientErrorException(statusLine, error); + } + } catch (IOException e) { + throw new ODataException(e.getMessage(), e); + } + } + + throw new ODataException(statusLine.getReasonPhrase()); + } + + return httpStatusCode; + } + + @Override + public final void completed(HttpResponse result) { + try { + // check response status + checkStatus(result); + + onCompleted(result); + } catch (Exception e) { + failed(e); + } finally { + if (result instanceof Closeable) { + try { + ((Closeable) result).close(); + } catch (final IOException ignore) { + } + } + } + } + + protected abstract void onCompleted(HttpResponse result) throws ODataException, IOException; + + @Override + public final void failed(Exception ex) { + responseHandler.onException(ex); + } + + @Override + public final void cancelled() { + responseHandler.onCanceled(); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/e6eded4c/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/impl/Olingo4AppImpl.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/impl/Olingo4AppImpl.java b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/impl/Olingo4AppImpl.java new file mode 100644 index 0000000..7dffd1c --- /dev/null +++ b/components/camel-olingo4/camel-olingo4-api/src/main/java/org/apache/camel/component/olingo4/api/impl/Olingo4AppImpl.java @@ -0,0 +1,841 @@ +/** + * 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.camel.component.olingo4.api.impl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.camel.component.olingo4.api.Olingo4App; +import org.apache.camel.component.olingo4.api.Olingo4ResponseHandler; +import org.apache.camel.component.olingo4.api.batch.Olingo4BatchChangeRequest; +import org.apache.camel.component.olingo4.api.batch.Olingo4BatchQueryRequest; +import org.apache.camel.component.olingo4.api.batch.Olingo4BatchRequest; +import org.apache.camel.component.olingo4.api.batch.Olingo4BatchResponse; +import org.apache.camel.component.olingo4.api.batch.Operation; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.LineIterator; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Consts; +import org.apache.http.Header; +import org.apache.http.HttpException; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.HttpResponseFactory; +import org.apache.http.HttpVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.entity.DecompressingEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.config.MessageConstraints; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.DefaultHttpResponseFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.DefaultHttpResponseParser; +import org.apache.http.impl.io.HttpTransportMetricsImpl; +import org.apache.http.impl.io.SessionInputBufferImpl; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.apache.http.message.BasicLineParser; +import org.apache.olingo.client.api.ODataBatchConstants; +import org.apache.olingo.client.api.ODataClient; +import org.apache.olingo.client.api.communication.request.ODataStreamer; +import org.apache.olingo.client.api.communication.request.batch.ODataBatchLineIterator; +import org.apache.olingo.client.api.domain.ClientEntity; +import org.apache.olingo.client.api.domain.ClientPrimitiveValue; +import org.apache.olingo.client.api.domain.ClientProperty; +import org.apache.olingo.client.api.serialization.ODataReader; +import org.apache.olingo.client.api.serialization.ODataWriter; +import org.apache.olingo.client.api.uri.SegmentType; +import org.apache.olingo.client.core.ODataClientFactory; +import org.apache.olingo.client.core.communication.request.batch.ODataBatchController; +import org.apache.olingo.client.core.communication.request.batch.ODataBatchLineIteratorImpl; +import org.apache.olingo.client.core.communication.request.batch.ODataBatchUtilities; +import org.apache.olingo.client.core.http.HttpMerge; +import org.apache.olingo.commons.api.Constants; +import org.apache.olingo.commons.api.edm.Edm; +import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind; +import org.apache.olingo.commons.api.edm.EdmReturnType; +import org.apache.olingo.commons.api.edm.constants.ODataServiceVersion; +import org.apache.olingo.commons.api.ex.ODataException; +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.uri.UriInfo; +import org.apache.olingo.server.api.uri.UriInfoKind; +import org.apache.olingo.server.api.uri.UriParameter; +import org.apache.olingo.server.api.uri.UriResource; +import org.apache.olingo.server.api.uri.UriResourceEntitySet; +import org.apache.olingo.server.api.uri.UriResourceKind; +import org.apache.olingo.server.core.uri.UriResourceFunctionImpl; +import org.apache.olingo.server.core.uri.parser.Parser; +import org.apache.olingo.server.core.uri.parser.UriParserException; +import org.apache.olingo.server.core.uri.validator.UriValidationException; + +/** + * Application API used by Olingo4 Component. + */ +public final class Olingo4AppImpl implements Olingo4App { + + private static final String SEPARATOR = "/"; + private static final String BOUNDARY_PREFIX = "batch_"; + private static final String BOUNDARY_PARAMETER = "; boundary="; + private static final String BOUNDARY_DOUBLE_DASH = "--"; + private static final String MULTIPART_MIME_TYPE = "multipart/"; + private static final String CONTENT_ID_HEADER = "Content-ID"; + private static final String CLIENT_ENTITY_FAKE_MARKER = "('X')"; + + private static final ContentType DEFAULT_CONTENT_TYPE = ContentType.create(ContentType.APPLICATION_JSON, ContentType.PARAMETER_CHARSET, Constants.UTF8); + private static final ContentType METADATA_CONTENT_TYPE = ContentType.create(ContentType.APPLICATION_XML, ContentType.PARAMETER_CHARSET, Constants.UTF8); + private static final ContentType TEXT_PLAIN_WITH_CS_UTF_8 = ContentType.create(ContentType.TEXT_PLAIN, ContentType.PARAMETER_CHARSET, Constants.UTF8); + private static final ContentType SERVICE_DOCUMENT_CONTENT_TYPE = ContentType.APPLICATION_JSON; + private static final ContentType BATCH_CONTENT_TYPE = ContentType.MULTIPART_MIXED; + + /** + * Reference to CloseableHttpAsyncClient (default) or CloseableHttpClient + */ + private final Closeable client; + + /** + * Reference to ODataClient reader and writer + */ + private final ODataClient odataClient = ODataClientFactory.getClient(); + private final ODataReader odataReader = odataClient.getReader(); + private final ODataWriter odataWriter = odataClient.getWriter(); + + private String serviceUri; + private ContentType contentType; + private Map<String, String> httpHeaders; + + /** + * Create Olingo4 Application with default HTTP configuration. + */ + public Olingo4AppImpl(String serviceUri) { + // By default create HTTP asynchronous client + this(serviceUri, (HttpClientBuilder)null); + } + + /** + * Create Olingo4 Application with custom HTTP Asynchronous client builder. + * + * @param serviceUri Service Application base URI. + * @param builder custom HTTP client builder. + */ + public Olingo4AppImpl(String serviceUri, HttpAsyncClientBuilder builder) { + setServiceUri(serviceUri); + + CloseableHttpAsyncClient asyncClient; + if (builder == null) { + asyncClient = HttpAsyncClients.createDefault(); + } else { + asyncClient = builder.build(); + } + asyncClient.start(); + this.client = asyncClient; + this.contentType = DEFAULT_CONTENT_TYPE; + } + + /** + * Create Olingo4 Application with custom HTTP Synchronous client builder. + * + * @param serviceUri Service Application base URI. + * @param builder Custom HTTP Synchronous client builder. + */ + public Olingo4AppImpl(String serviceUri, HttpClientBuilder builder) { + setServiceUri(serviceUri); + + if (builder == null) { + this.client = HttpClients.createDefault(); + } else { + this.client = builder.build(); + } + this.contentType = DEFAULT_CONTENT_TYPE; + } + + @Override + public void setServiceUri(String serviceUri) { + if (serviceUri == null || serviceUri.isEmpty()) { + throw new IllegalArgumentException("serviceUri"); + } + this.serviceUri = serviceUri.endsWith(SEPARATOR) ? serviceUri.substring(0, serviceUri.length() - 1) : serviceUri; + } + + @Override + public String getServiceUri() { + return serviceUri; + } + + @Override + public Map<String, String> getHttpHeaders() { + return httpHeaders; + } + + @Override + public void setHttpHeaders(Map<String, String> httpHeaders) { + this.httpHeaders = httpHeaders; + } + + @Override + public String getContentType() { + return contentType.toContentTypeString(); + } + + @Override + public void setContentType(String contentType) { + this.contentType = ContentType.parse(contentType); + } + + @Override + public void close() { + + } + + @Override + public <T> void read(final Edm edm, final String resourcePath, final Map<String, String> queryParams, final Olingo4ResponseHandler<T> responseHandler) { + final String queryOptions = concatQueryParams(queryParams); + final UriInfo uriInfo = parseUri(edm, resourcePath, queryOptions); + + execute(new HttpGet(createUri(resourcePath, queryOptions)), getResourceContentType(uriInfo), new AbstractFutureCallback<T>(responseHandler) { + + @Override + public void onCompleted(HttpResponse result) throws IOException { + readContent(uriInfo, result.getEntity() != null ? result.getEntity().getContent() : null, responseHandler); + } + + }); + } + + @Override + public void uread(final Edm edm, final String resourcePath, final Map<String, String> queryParams, final Olingo4ResponseHandler<InputStream> responseHandler) { + final String queryOptions = concatQueryParams(queryParams); + final UriInfo uriInfo = parseUri(edm, resourcePath, queryOptions); + + execute(new HttpGet(createUri(resourcePath, queryOptions)), getResourceContentType(uriInfo), new AbstractFutureCallback<InputStream>(responseHandler) { + + @Override + public void onCompleted(HttpResponse result) throws IOException { + InputStream responseStream = result.getEntity() != null ? result.getEntity().getContent() : null; + if (responseStream != null && result.getEntity() instanceof DecompressingEntity) { + // In case of GZIP compression it's necessary to create + // InputStream from the source byte array + responseHandler.onResponse(new ByteArrayInputStream(IOUtils.toByteArray(responseStream))); + } else { + responseHandler.onResponse(responseStream); + } + } + }); + } + + @Override + public <T> void create(Edm edm, String resourcePath, Object data, Olingo4ResponseHandler<T> responseHandler) { + final UriInfo uriInfo = parseUri(edm, resourcePath, null); + + writeContent(edm, new HttpPost(createUri(resourcePath, null)), uriInfo, data, responseHandler); + } + + @Override + public <T> void update(Edm edm, String resourcePath, Object data, Olingo4ResponseHandler<T> responseHandler) { + final UriInfo uriInfo = parseUri(edm, resourcePath, null); + + writeContent(edm, new HttpPut(createUri(resourcePath, null)), uriInfo, data, responseHandler); + } + + @Override + public void delete(String resourcePath, final Olingo4ResponseHandler<HttpStatusCode> responseHandler) { + execute(new HttpDelete(createUri(resourcePath)), contentType, new AbstractFutureCallback<HttpStatusCode>(responseHandler) { + @Override + public void onCompleted(HttpResponse result) { + final StatusLine statusLine = result.getStatusLine(); + responseHandler.onResponse(HttpStatusCode.fromStatusCode(statusLine.getStatusCode())); + } + }); + } + + @Override + public <T> void patch(Edm edm, String resourcePath, Object data, Olingo4ResponseHandler<T> responseHandler) { + final UriInfo uriInfo = parseUri(edm, resourcePath, null); + + writeContent(edm, new HttpPatch(createUri(resourcePath, null)), uriInfo, data, responseHandler); + } + + @Override + public <T> void merge(Edm edm, String resourcePath, Object data, Olingo4ResponseHandler<T> responseHandler) { + final UriInfo uriInfo = parseUri(edm, resourcePath, null); + + writeContent(edm, new HttpMerge(createUri(resourcePath, null)), uriInfo, data, responseHandler); + } + + @Override + public void batch(Edm edm, Object data, Olingo4ResponseHandler<List<Olingo4BatchResponse>> responseHandler) { + final UriInfo uriInfo = parseUri(edm, SegmentType.BATCH.getValue(), null); + + writeContent(edm, new HttpPost(createUri(SegmentType.BATCH.getValue(), null)), uriInfo, data, responseHandler); + } + + private ContentType getResourceContentType(UriInfo uriInfo) { + ContentType resourceContentType; + switch (uriInfo.getKind()) { + case service: + // service document + resourceContentType = SERVICE_DOCUMENT_CONTENT_TYPE; + break; + case metadata: + // metadata + resourceContentType = METADATA_CONTENT_TYPE; + break; + case resource: + List<UriResource> listResource = uriInfo.getUriResourceParts(); + UriResourceKind lastResourceKind = listResource.get(listResource.size() - 1).getKind(); + // is it a $value or $count URI?? + if (lastResourceKind == UriResourceKind.count || lastResourceKind == UriResourceKind.value) { + resourceContentType = TEXT_PLAIN_WITH_CS_UTF_8; + } else { + resourceContentType = contentType; + } + break; + default: + resourceContentType = contentType; + } + return resourceContentType; + } + + private <T> void readContent(UriInfo uriInfo, InputStream content, Olingo4ResponseHandler<T> responseHandler) { + try { + responseHandler.onResponse(this.<T> readContent(uriInfo, content)); + } catch (ODataException e) { + responseHandler.onException(e); + } + } + + @SuppressWarnings("unchecked") + private <T> T readContent(UriInfo uriInfo, InputStream content) throws ODataException { + T response = null; + switch (uriInfo.getKind()) { + case service: + // service document + response = (T)odataReader.readServiceDocument(content, SERVICE_DOCUMENT_CONTENT_TYPE); + break; + + case metadata: + // $metadata + response = (T)odataReader.readMetadata(content); + break; + case resource: + // any resource entity + List<UriResource> listResource = uriInfo.getUriResourceParts(); + UriResourceKind lastResourceKind = listResource.get(listResource.size() - 1).getKind(); + switch (lastResourceKind) { + case entitySet: + UriResourceEntitySet uriResourceEntitySet = (UriResourceEntitySet)listResource.get(listResource.size() - 1); + List<UriParameter> keyPredicates = uriResourceEntitySet.getKeyPredicates(); + // Check result type: single Entity or EntitySet based + // on key predicate detection + if (keyPredicates.size() == 1) { + response = (T)odataReader.readEntity(content, getResourceContentType(uriInfo)); + } else { + response = (T)odataReader.readEntitySet(content, getResourceContentType(uriInfo)); + } + break; + case count: + String stringCount = null; + try { + stringCount = IOUtils.toString(content, Consts.UTF_8); + response = (T)Long.valueOf(stringCount); + } catch (IOException e) { + throw new ODataException("Error during $count value deserialization", e); + } catch (NumberFormatException e) { + throw new ODataException("Error during $count value conversion: " + stringCount, e); + } + break; + case value: + try { + ClientPrimitiveValue value = odataClient.getObjectFactory().newPrimitiveValueBuilder().setType(EdmPrimitiveTypeKind.String) + .setValue(IOUtils.toString(content, Consts.UTF_8)).build(); + response = (T)value; + } catch (IOException e) { + throw new ODataException("Error during $value deserialization", e); + } + break; + case primitiveProperty: + case complexProperty: + ClientProperty property = odataReader.readProperty(content, getResourceContentType(uriInfo)); + if (property.hasPrimitiveValue()) { + response = (T)property.getPrimitiveValue(); + } else if (property.hasComplexValue()) { + response = (T)property.getComplexValue(); + } else { + throw new ODataException("Unsupported property: " + property.getName()); + } + break; + case function: + UriResourceFunctionImpl uriResourceFunction = (UriResourceFunctionImpl)listResource.get(listResource.size() - 1); + EdmReturnType functionReturnType = uriResourceFunction.getFunction().getReturnType(); + + switch (functionReturnType.getType().getKind()) { + case ENTITY: + if (functionReturnType.isCollection()) { + response = (T)odataReader.readEntitySet(content, getResourceContentType(uriInfo)); + } else { + response = (T)odataReader.readEntity(content, getResourceContentType(uriInfo)); + } + break; + case PRIMITIVE: + case COMPLEX: + ClientProperty functionProperty = odataReader.readProperty(content, getResourceContentType(uriInfo)); + if (functionProperty.hasPrimitiveValue()) { + response = (T)functionProperty.getPrimitiveValue(); + } else if (functionProperty.hasComplexValue()) { + response = (T)functionProperty.getComplexValue(); + } else { + throw new ODataException("Unsupported property: " + functionProperty.getName()); + } + break; + default: + throw new ODataException("Unsupported function return type " + uriInfo.getKind().name()); + } + break; + default: + throw new ODataException("Unsupported resource type: " + lastResourceKind.name()); + } + break; + + default: + throw new ODataException("Unsupported resource type " + uriInfo.getKind().name()); + } + + return response; + } + + private <T> void writeContent(final Edm edm, HttpEntityEnclosingRequestBase httpEntityRequest, final UriInfo uriInfo, final Object content, + final Olingo4ResponseHandler<T> responseHandler) { + + try { + httpEntityRequest.setEntity(writeContent(edm, uriInfo, content)); + + final Header requestContentTypeHeader = httpEntityRequest.getEntity().getContentType(); + final ContentType requestContentType = requestContentTypeHeader != null ? ContentType.parse(requestContentTypeHeader.getValue()) : contentType; + + execute(httpEntityRequest, requestContentType, new AbstractFutureCallback<T>(responseHandler) { + @SuppressWarnings("unchecked") + @Override + public void onCompleted(HttpResponse result) throws IOException, ODataException { + + // if a entity is created (via POST request) the response + // body contains the new created entity + HttpStatusCode statusCode = HttpStatusCode.fromStatusCode(result.getStatusLine().getStatusCode()); + + // look for no content, or no response body!!! + final boolean noEntity = result.getEntity() == null || result.getEntity().getContentLength() == 0; + if (statusCode == HttpStatusCode.NO_CONTENT || noEntity) { + responseHandler.onResponse((T)HttpStatusCode.fromStatusCode(result.getStatusLine().getStatusCode())); + } else { + if (uriInfo.getKind() == UriInfoKind.resource) { + List<UriResource> listResource = uriInfo.getUriResourceParts(); + UriResourceKind lastResourceKind = listResource.get(listResource.size() - 1).getKind(); + switch (lastResourceKind) { + case entitySet: + if (content instanceof ClientEntity) { + ClientEntity entity = odataReader.readEntity(result.getEntity().getContent(), + ContentType.parse(result.getEntity().getContentType().getValue())); + responseHandler.onResponse((T)entity); + } else { + throw new ODataException("Unsupported content type: " + content); + } + break; + default: + break; + } + } else if (uriInfo.getKind() == UriInfoKind.batch) { + List<Olingo4BatchResponse> batchResponse = parseBatchResponse(edm, result, (List<Olingo4BatchRequest>)content); + responseHandler.onResponse((T)batchResponse); + } else { + throw new ODataException("Unsupported resource type: " + uriInfo.getKind().name()); + } + } + } + }); + + } catch (ODataException e) { + responseHandler.onException(e); + } + } + + private AbstractHttpEntity writeContent(final Edm edm, final UriInfo uriInfo, final Object content) throws ODataException { + InputStream requestStream = null; + AbstractHttpEntity httpEntity = null; + if (uriInfo.getKind() == UriInfoKind.resource) { + // any resource entity + List<UriResource> listResource = uriInfo.getUriResourceParts(); + UriResourceKind lastResourceKind = listResource.get(listResource.size() - 1).getKind(); + switch (lastResourceKind) { + case entitySet: + if (content instanceof ClientEntity) { + requestStream = odataWriter.writeEntity((ClientEntity)content, getResourceContentType(uriInfo)); + } else { + throw new ODataException("Unsupported content type: " + content); + } + break; + default: + throw new ODataException("Unsupported resource type: " + lastResourceKind); + } + try { + httpEntity = new ByteArrayEntity(IOUtils.toByteArray(requestStream)); + } catch (IOException e) { + throw new ODataException("Error during converting input stream to byte array", e); + } + httpEntity.setChunked(false); + + } else if (uriInfo.getKind() == UriInfoKind.batch) { + final String boundary = BOUNDARY_PREFIX + UUID.randomUUID(); + final String contentHeader = BATCH_CONTENT_TYPE + BOUNDARY_PARAMETER + boundary; + final List<Olingo4BatchRequest> batchParts = (List<Olingo4BatchRequest>)content; + + requestStream = serializeBatchRequest(edm, batchParts, BOUNDARY_DOUBLE_DASH + boundary); + try { + httpEntity = new ByteArrayEntity(IOUtils.toByteArray(requestStream)); + } catch (IOException e) { + throw new ODataException("Error during converting input stream to byte array", e); + } + httpEntity.setChunked(false); + httpEntity.setContentType(contentHeader); + } else { + throw new ODataException("Unsupported resource type: " + uriInfo.getKind().name()); + } + + return httpEntity; + } + + private InputStream serializeBatchRequest(final Edm edm, final List<Olingo4BatchRequest> batchParts, String boundary) throws ODataException { + final ByteArrayOutputStream batchRequestHeaderOutputStream = new ByteArrayOutputStream(); + + try { + batchRequestHeaderOutputStream.write(boundary.getBytes(Constants.UTF8)); + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + + for (Olingo4BatchRequest batchPart : batchParts) { + writeHttpHeader(batchRequestHeaderOutputStream, HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_HTTP.toContentTypeString()); + writeHttpHeader(batchRequestHeaderOutputStream, ODataBatchConstants.ITEM_TRANSFER_ENCODING_LINE, null); + + if (batchPart instanceof Olingo4BatchQueryRequest) { + final Olingo4BatchQueryRequest batchQueryPart = (Olingo4BatchQueryRequest)batchPart; + final String batchQueryUri = createUri(StringUtils.isBlank(batchQueryPart.getResourceUri()) ? serviceUri : batchQueryPart.getResourceUri(), + batchQueryPart.getResourcePath(), concatQueryParams(batchQueryPart.getQueryParams())); + final UriInfo uriInfo = parseUri(edm, batchQueryPart.getResourcePath(), concatQueryParams(batchQueryPart.getQueryParams())); + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + + batchRequestHeaderOutputStream.write((HttpGet.METHOD_NAME + " " + batchQueryUri + " " + HttpVersion.HTTP_1_1).getBytes(Constants.UTF8)); + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + writeHttpHeader(batchRequestHeaderOutputStream, HttpHeaders.ACCEPT, getResourceContentType(uriInfo).toContentTypeString()); + + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + batchRequestHeaderOutputStream.write(boundary.getBytes(Constants.UTF8)); + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + } else if (batchPart instanceof Olingo4BatchChangeRequest) { + final Olingo4BatchChangeRequest batchChangePart = (Olingo4BatchChangeRequest)batchPart; + final String batchChangeUri = createUri(StringUtils.isBlank(batchChangePart.getResourceUri()) ? serviceUri : batchChangePart.getResourceUri(), + batchChangePart.getResourcePath(), null); + final UriInfo uriInfo = parseUri(edm, batchChangePart.getResourcePath(), null); + + if (batchChangePart.getOperation() != Operation.DELETE) { + writeHttpHeader(batchRequestHeaderOutputStream, CONTENT_ID_HEADER, batchChangePart.getContentId()); + } + + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + batchRequestHeaderOutputStream + .write((batchChangePart.getOperation().getHttpMethod() + " " + batchChangeUri + " " + HttpVersion.HTTP_1_1).getBytes(Constants.UTF8)); + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + writeHttpHeader(batchRequestHeaderOutputStream, HttpHeader.ODATA_VERSION, ODataServiceVersion.V40.toString()); + writeHttpHeader(batchRequestHeaderOutputStream, HttpHeaders.ACCEPT, getResourceContentType(uriInfo).toContentTypeString()); + writeHttpHeader(batchRequestHeaderOutputStream, HttpHeaders.CONTENT_TYPE, getResourceContentType(uriInfo).toContentTypeString()); + + if (batchChangePart.getOperation() != Operation.DELETE) { + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + AbstractHttpEntity httpEnity = writeContent(edm, uriInfo, batchChangePart.getBody()); + + batchRequestHeaderOutputStream.write(IOUtils.toByteArray(httpEnity.getContent())); + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + } else { + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + } + + batchRequestHeaderOutputStream.write(boundary.getBytes(Constants.UTF8)); + batchRequestHeaderOutputStream.write(ODataStreamer.CRLF); + } else { + throw new ODataException("Unsupported batch part request object type: " + batchPart); + } + } + } catch (Exception e) { + throw new ODataException("Error during batch request serialization", e); + } + return new ByteArrayInputStream(batchRequestHeaderOutputStream.toByteArray()); + } + + private void writeHttpHeader(ByteArrayOutputStream headerOutputStream, String headerName, String headerValue) throws IOException { + headerOutputStream.write(createHttpHeader(headerName, headerValue).getBytes(Constants.UTF8)); + headerOutputStream.write(ODataStreamer.CRLF); + } + + private String createHttpHeader(String headerName, String headerValue) { + return headerName + (StringUtils.isBlank(headerValue) ? "" : (": " + headerValue)); + } + + private List<Olingo4BatchResponse> parseBatchResponse(final Edm edm, HttpResponse response, List<Olingo4BatchRequest> batchRequest) throws ODataException { + List<Olingo4BatchResponse> batchResponse = new <Olingo4BatchResponse> ArrayList(); + try { + final Header[] contentHeaders = response.getHeaders(HttpHeader.CONTENT_TYPE); + final ODataBatchLineIterator batchLineIterator = new ODataBatchLineIteratorImpl(IOUtils.lineIterator(response.getEntity().getContent(), Constants.UTF8)); + final String batchBoundary = ODataBatchUtilities.getBoundaryFromHeader(getHeadersCollection(contentHeaders)); + final ODataBatchController batchController = new ODataBatchController(batchLineIterator, batchBoundary); + + batchController.getBatchLineIterator().next(); + int batchRequestIndex = 0; + while (batchController.getBatchLineIterator().hasNext()) { + OutputStream os = new ByteArrayOutputStream(); + ODataBatchUtilities.readBatchPart(batchController, os, false); + Object content = null; + final Olingo4BatchRequest batchPartRequest = (Olingo4BatchRequest)batchRequest.get(batchRequestIndex); + final HttpResponse batchPartHttpResponse = constructBatchPartHttpResponse(new ByteArrayInputStream(((ByteArrayOutputStream)os).toByteArray())); + final StatusLine batchPartStatusLine = batchPartHttpResponse.getStatusLine(); + final int batchPartLineStatusCode = batchPartStatusLine.getStatusCode(); + Map<String, String> batchPartHeaders = getHeadersValueMap(batchPartHttpResponse.getAllHeaders()); + if (batchPartRequest instanceof Olingo4BatchQueryRequest) { + Olingo4BatchQueryRequest batchPartQueryRequest = (Olingo4BatchQueryRequest)batchPartRequest; + final UriInfo uriInfo = parseUri(edm, batchPartQueryRequest.getResourcePath(), null); + + if (HttpStatusCode.BAD_REQUEST.getStatusCode() <= batchPartLineStatusCode && batchPartLineStatusCode <= AbstractFutureCallback.NETWORK_CONNECT_TIMEOUT_ERROR) { + final ContentType responseContentType = ContentType.parse(batchPartHttpResponse.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()); + content = odataReader.readError(batchPartHttpResponse.getEntity().getContent(), responseContentType); + } else if (batchPartLineStatusCode == HttpStatusCode.NO_CONTENT.getStatusCode()) { + // nothing to do if NO_CONTENT returning + } else { + content = readContent(uriInfo, batchPartHttpResponse.getEntity().getContent()); + } + + Olingo4BatchResponse batchPartResponse = new Olingo4BatchResponse(batchPartStatusLine.getStatusCode(), batchPartStatusLine.getReasonPhrase(), null, + batchPartHeaders, content); + batchResponse.add(batchPartResponse); + } else if (batchPartRequest instanceof Olingo4BatchChangeRequest) { + Olingo4BatchChangeRequest batchPartChangeRequest = (Olingo4BatchChangeRequest)batchPartRequest; + + if (batchPartLineStatusCode != HttpStatusCode.NO_CONTENT.getStatusCode()) { + if (HttpStatusCode.BAD_REQUEST.getStatusCode() <= batchPartLineStatusCode + && batchPartLineStatusCode <= AbstractFutureCallback.NETWORK_CONNECT_TIMEOUT_ERROR) { + final ContentType responseContentType = ContentType.parse(batchPartHttpResponse.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()); + content = odataReader.readError(response.getEntity().getContent(), responseContentType); + } else { + final UriInfo uriInfo = parseUri(edm, batchPartChangeRequest.getResourcePath() + + (batchPartChangeRequest.getOperation() == Operation.CREATE ? CLIENT_ENTITY_FAKE_MARKER : ""), + null); + content = readContent(uriInfo, batchPartHttpResponse.getEntity().getContent()); + } + } + Olingo4BatchResponse batchPartResponse = new Olingo4BatchResponse(batchPartStatusLine.getStatusCode(), batchPartStatusLine.getReasonPhrase(), + batchPartChangeRequest.getContentId(), batchPartHeaders, content); + batchResponse.add(batchPartResponse); + } else { + throw new ODataException("Unsupported batch part request object type: " + batchPartRequest); + } + batchRequestIndex++; + } + + } catch (IOException | HttpException e) { + throw new ODataException(e); + } + return batchResponse; + } + + private HttpResponse constructBatchPartHttpResponse(InputStream batchPartStream) throws IOException, HttpException { + final LineIterator lines = IOUtils.lineIterator(batchPartStream, Constants.UTF8); + final ByteArrayOutputStream headerOutputStream = new ByteArrayOutputStream(); + final ByteArrayOutputStream bodyOutputStream = new ByteArrayOutputStream(); + + boolean startBatchPartHeader = false; + boolean startBatchPartBody = false; + + // Iterate through lines in the batch part + while (lines.hasNext()) { + String line = lines.nextLine().trim(); + // Ignore all lines below HTTP/1.1 line + if (line.startsWith(HttpVersion.HTTP)) { + // This is the first header line + startBatchPartHeader = true; + } + // Body starts with empty string after header lines + if (startBatchPartHeader && StringUtils.isBlank(line)) { + startBatchPartHeader = false; + startBatchPartBody = true; + } + if (startBatchPartHeader) { + // Write header to the output stream + headerOutputStream.write(line.getBytes(Constants.UTF8)); + headerOutputStream.write(ODataStreamer.CRLF); + } else if (startBatchPartBody && StringUtils.isNotBlank(line)) { + // Write body to the output stream + bodyOutputStream.write(line.getBytes(Constants.UTF8)); + bodyOutputStream.write(ODataStreamer.CRLF); + } + } + + // Prepare for parsing headers in to the HttpResponse object + HttpTransportMetricsImpl metrics = new HttpTransportMetricsImpl(); + SessionInputBufferImpl sessionInputBuffer = new SessionInputBufferImpl(metrics, 2048); + HttpResponseFactory responseFactory = new DefaultHttpResponseFactory(); + + sessionInputBuffer.bind(new ByteArrayInputStream(headerOutputStream.toByteArray())); + DefaultHttpResponseParser responseParser = new DefaultHttpResponseParser(sessionInputBuffer, new BasicLineParser(), responseFactory, MessageConstraints.DEFAULT); + + // Parse HTTP response and headers + HttpResponse response = responseParser.parse(); + // Set body inside entity + response.setEntity(new ByteArrayEntity(bodyOutputStream.toByteArray())); + + return response; + } + + private Collection<String> getHeadersCollection(Header[] headers) { + Collection<String> headersCollection = new ArrayList(); + for (Header header : Arrays.asList(headers)) { + headersCollection.add(header.getValue()); + } + return headersCollection; + } + + private Map<String, String> getHeadersValueMap(Header[] headers) { + Map<String, String> headersValueMap = new HashMap(); + for (Header header : Arrays.asList(headers)) { + headersValueMap.put(header.getName(), header.getValue()); + } + return headersValueMap; + } + + private String createUri(String resourcePath) { + return createUri(serviceUri, resourcePath, null); + } + + private String createUri(String resourcePath, String queryOptions) { + return createUri(serviceUri, resourcePath, queryOptions); + } + + private String createUri(String resourceUri, String resourcePath, String queryOptions) { + + final StringBuilder absolutUri = new StringBuilder(resourceUri).append(SEPARATOR).append(resourcePath); + if (queryOptions != null && !queryOptions.isEmpty()) { + absolutUri.append("/?" + queryOptions); + } + return absolutUri.toString(); + + } + + private String concatQueryParams(final Map<String, String> queryParams) { + final StringBuilder concatQuery = new StringBuilder(""); + if (queryParams != null && !queryParams.isEmpty()) { + int nParams = queryParams.size(); + int index = 0; + for (Map.Entry<String, String> entry : queryParams.entrySet()) { + concatQuery.append(entry.getKey()).append('=').append(entry.getValue()); + if (++index < nParams) { + concatQuery.append('&'); + } + } + } + return concatQuery.toString().replaceAll(" *", "%20"); + } + + private static UriInfo parseUri(Edm edm, String resourcePath, String queryOptions) { + Parser parser = new Parser(edm, OData.newInstance()); + UriInfo result; + + try { + result = parser.parseUri(resourcePath, queryOptions, null); + } catch (UriParserException | UriValidationException e) { + throw new IllegalArgumentException("parseUri (" + resourcePath + "," + queryOptions + "): " + e.getMessage(), e); + } + return result; + } + + public void execute(HttpUriRequest httpUriRequest, ContentType contentType, FutureCallback<HttpResponse> callback) { + // add accept header when its not a form or multipart + final String contentTypeString = contentType.toString(); + if (!ContentType.APPLICATION_FORM_URLENCODED.equals(contentType) && !contentType.toContentTypeString().startsWith(MULTIPART_MIME_TYPE)) { + // otherwise accept what is being sent + httpUriRequest.addHeader(HttpHeaders.ACCEPT, contentTypeString); + } + + // is something being sent? + if (httpUriRequest instanceof HttpEntityEnclosingRequestBase && httpUriRequest.getFirstHeader(HttpHeaders.CONTENT_TYPE) == null) { + httpUriRequest.addHeader(HttpHeaders.CONTENT_TYPE, contentTypeString); + } + + // set user specified custom headers + if (httpHeaders != null && !httpHeaders.isEmpty()) { + for (Map.Entry<String, String> entry : httpHeaders.entrySet()) { + httpUriRequest.setHeader(entry.getKey(), entry.getValue()); + } + } + + // add 'Accept-Charset' header to avoid BOM marker presents inside + // response stream + if (!httpUriRequest.containsHeader(HttpHeaders.ACCEPT_CHARSET)) { + httpUriRequest.addHeader(HttpHeaders.ACCEPT_CHARSET, Constants.UTF8); + } + + // add client protocol version if not specified + if (!httpUriRequest.containsHeader(HttpHeader.ODATA_VERSION)) { + httpUriRequest.addHeader(HttpHeader.ODATA_VERSION, ODataServiceVersion.V40.toString()); + } + if (!httpUriRequest.containsHeader(HttpHeader.ODATA_MAX_VERSION)) { + httpUriRequest.addHeader(HttpHeader.ODATA_MAX_VERSION, ODataServiceVersion.V40.toString()); + } + + // execute request + if (client instanceof CloseableHttpAsyncClient) { + ((CloseableHttpAsyncClient)client).execute(httpUriRequest, callback); + } else { + // invoke the callback methods explicitly after executing the + // request synchronously + try { + CloseableHttpResponse result = ((CloseableHttpClient)client).execute(httpUriRequest); + callback.completed(result); + } catch (IOException e) { + callback.failed(e); + } + } + } +}