CAMEL-7773 Initial version of Camel-Olingo2 component
Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/9a92064c Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/9a92064c Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/9a92064c Branch: refs/heads/master Commit: 9a92064cf9977a1cafa916441995e5e7804ec19e Parents: 89a8b35 Author: Dhiraj Bokde <dhira...@yahoo.com> Authored: Mon Sep 1 00:18:26 2014 -0700 Committer: Dhiraj Bokde <dhira...@yahoo.com> Committed: Mon Sep 1 00:18:26 2014 -0700 ---------------------------------------------------------------------- .../camel-olingo2/camel-olingo2-api/pom.xml | 141 +++ .../camel/component/olingo2/api/Olingo2App.java | 134 +++ .../olingo2/api/Olingo2ResponseHandler.java | 43 + .../api/batch/Olingo2BatchChangeRequest.java | 94 ++ .../api/batch/Olingo2BatchQueryRequest.java | 71 ++ .../olingo2/api/batch/Olingo2BatchRequest.java | 39 + .../olingo2/api/batch/Olingo2BatchResponse.java | 65 ++ .../component/olingo2/api/batch/Operation.java | 35 + .../api/impl/AbstractFutureCallback.java | 104 +++ .../component/olingo2/api/impl/HttpMerge.java | 47 + .../olingo2/api/impl/Olingo2AppImpl.java | 901 +++++++++++++++++++ .../olingo2/api/Olingo2AppIntegrationTest.java | 560 ++++++++++++ .../src/test/resources/log4j.properties | 39 + .../camel-olingo2-component/pom.xml | 208 +++++ .../component/olingo2/Olingo2Component.java | 152 ++++ .../component/olingo2/Olingo2Configuration.java | 158 ++++ .../component/olingo2/Olingo2Consumer.java | 93 ++ .../component/olingo2/Olingo2Endpoint.java | 281 ++++++ .../component/olingo2/Olingo2Producer.java | 106 +++ .../olingo2/internal/Olingo2Constants.java | 29 + .../internal/Olingo2PropertiesHelper.java | 39 + .../services/org/apache/camel/component/olingo2 | 1 + .../src/signatures/olingo-api-signature.txt | 7 + .../olingo2/AbstractOlingo2TestSupport.java | 82 ++ .../olingo2/Olingo2AppIntegrationTest.java | 235 +++++ .../src/test/resources/log4j.properties | 39 + .../src/test/resources/test-options.properties | 19 + components/camel-olingo2/pom.xml | 65 ++ components/pom.xml | 1 + 29 files changed, 3788 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/9a92064c/components/camel-olingo2/camel-olingo2-api/pom.xml ---------------------------------------------------------------------- diff --git a/components/camel-olingo2/camel-olingo2-api/pom.xml b/components/camel-olingo2/camel-olingo2-api/pom.xml new file mode 100644 index 0000000..971e402 --- /dev/null +++ b/components/camel-olingo2/camel-olingo2-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.component.olingo2</groupId> + <artifactId>camel-olingo2-parent</artifactId> + <version>2.14-SNAPSHOT</version> + </parent> + + <artifactId>camel-olingo2-api</artifactId> + <name>Camel :: Olingo2 Component API</name> + <description>Camel API for Apache Olingo2</description> + <packaging>bundle</packaging> + + <properties> + <camel.osgi.export.pkg>org.apache.camel.component.olingo2.api*</camel.osgi.export.pkg> + <olingo2-version>2.0.0</olingo2-version> + <http-asyncclient-version>4.0.2</http-asyncclient-version> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.olingo</groupId> + <artifactId>olingo-odata2-api</artifactId> + <version>${olingo2-version}</version> + </dependency> + <dependency> + <groupId>org.apache.olingo</groupId> + <artifactId>olingo-odata2-core</artifactId> + <version>${olingo2-version}</version> + <exclusions> + <exclusion> + <groupId>javax.ws.rs</groupId> + <artifactId>javax.ws.rs-api</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpasyncclient</artifactId> + <version>${http-asyncclient-version}</version> + </dependency> + + <!-- logging --> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>1.7.7</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + <version>1.7.7</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>log4j</groupId> + <artifactId>log4j</artifactId> + <version>1.2.17</version> + <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> + <version>2.9.1</version> + <executions> + <execution> + <id>add-javadoc</id> + <goals> + <goal>jar</goal> + </goals> + <configuration> + <attach>true</attach> + <source>1.6</source> + <quiet>true</quiet> + <detectOfflineLinks>false</detectOfflineLinks> + <javadocVersion>1.6</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/9a92064c/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/Olingo2App.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/Olingo2App.java b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/Olingo2App.java new file mode 100644 index 0000000..751b798 --- /dev/null +++ b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/Olingo2App.java @@ -0,0 +1,134 @@ +/** + * 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.olingo2.api; + +import java.util.List; +import java.util.Map; + +import org.apache.camel.component.olingo2.api.batch.Olingo2BatchResponse; +import org.apache.olingo.odata2.api.commons.HttpStatusCodes; +import org.apache.olingo.odata2.api.edm.Edm; + +/** + * Olingo2 Client Api Interface. + */ +public interface Olingo2App { + + /** + * 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 + * from http://www.odata.org/documentation/odata-version-2-0/uri-conventions#SystemQueryOptions + * @param responseHandler callback handler + */ + <T> void read(Edm edm, String resourcePath, Map<String, String> queryParams, + Olingo2ResponseHandler<T> responseHandler); + + /** + * Deletes an OData resource and invokes callback + * with {@link org.apache.olingo.odata2.api.commons.HttpStatusCodes} on success, or with exception on failure. + * @param resourcePath resource path for Entry + * @param responseHandler {@link org.apache.olingo.odata2.api.commons.HttpStatusCodes} callback handler + */ + void delete(String resourcePath, Olingo2ResponseHandler<HttpStatusCodes> 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, Olingo2ResponseHandler<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.odata2.api.ep.entry.ODataEntry} callback handler + */ + <T> void update(Edm edm, String resourcePath, Object data, Olingo2ResponseHandler<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.odata2.api.ep.entry.ODataEntry} callback handler + */ + <T> void patch(Edm edm, String resourcePath, Object data, Olingo2ResponseHandler<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.odata2.api.ep.entry.ODataEntry} callback handler + */ + <T> void merge(Edm edm, String resourcePath, Object data, Olingo2ResponseHandler<T> responseHandler); + + /** + * Executes a batch request. + * @param edm service Edm + * @param data ordered {@link org.apache.camel.component.olingo2.api.batch.Olingo2BatchRequest} list + * @param responseHandler callback handler + */ + void batch(Edm edm, Object data, Olingo2ResponseHandler<List<Olingo2BatchResponse>> responseHandler); +} http://git-wip-us.apache.org/repos/asf/camel/blob/9a92064c/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/Olingo2ResponseHandler.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/Olingo2ResponseHandler.java b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/Olingo2ResponseHandler.java new file mode 100644 index 0000000..e96e345 --- /dev/null +++ b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/Olingo2ResponseHandler.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.olingo2.api; + +/** + * Callback interface to asynchronously process Olingo2 response. + */ +public interface Olingo2ResponseHandler<T> { + + /** + * Handle response data on successful completion of Olingo2 request. + * @param response response data from Olingo2, may be NULL for Olingo2 operations with no response data. + */ + void onResponse(T response); + + /** + * Handle exception raised from Olingo2 request. + * @param ex exception from Olingo2 request. + * May be an instance of {@link org.apache.olingo.odata2.api.exception.ODataException} or + * some other exception, such as {@link java.io.IOException} + */ + void onException(Exception ex); + + /** + * Handle Olingo2 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/9a92064c/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchChangeRequest.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchChangeRequest.java b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchChangeRequest.java new file mode 100644 index 0000000..d2d06ad --- /dev/null +++ b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchChangeRequest.java @@ -0,0 +1,94 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.olingo2.api.batch; + +import java.util.Map; + +/** + * Batch Change part. + */ +public class Olingo2BatchChangeRequest extends Olingo2BatchRequest { + + 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(resourcePath) + .append(", headers=").append(headers) + .append(", contentId=").append(contentId) + .append(", operation=").append(operation) + .append(", body=").append(body) + .append('}') + .toString(); + } + + public static Olingo2BatchChangeRequestBuilder resourcePath(String resourcePath) { + if (resourcePath == null) { + throw new IllegalArgumentException("resourcePath"); + } + return new Olingo2BatchChangeRequestBuilder().resourcePath(resourcePath); + } + + public static class Olingo2BatchChangeRequestBuilder { + + private Olingo2BatchChangeRequest request = new Olingo2BatchChangeRequest(); + + public Olingo2BatchChangeRequestBuilder resourcePath(String resourcePath) { + request.resourcePath = resourcePath; + return this; + } + + public Olingo2BatchChangeRequestBuilder headers(Map<String, String> headers) { + request.headers = headers; + return this; + } + + public Olingo2BatchChangeRequestBuilder contentId(String contentId) { + request.contentId = contentId; + return this; + } + + public Olingo2BatchChangeRequestBuilder operation(Operation operation) { + request.operation = operation; + return this; + } + + public Olingo2BatchChangeRequestBuilder body(Object body) { + request.body = body; + return this; + } + + public Olingo2BatchChangeRequest build() { + return request; + } + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/9a92064c/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchQueryRequest.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchQueryRequest.java b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchQueryRequest.java new file mode 100644 index 0000000..cac47ff --- /dev/null +++ b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchQueryRequest.java @@ -0,0 +1,71 @@ +/** + * 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.olingo2.api.batch; + +import java.util.Map; + +/** + * Batch Query part. + */ +public class Olingo2BatchQueryRequest extends Olingo2BatchRequest { + + private Map<String, String> queryParams; + + public Map<String, String> getQueryParams() { + return queryParams; + } + + public static Olingo2BatchQueryRequestBuilder resourcePath(String resourcePath) { + if (resourcePath == null) { + throw new IllegalArgumentException("resourcePath"); + } + return new Olingo2BatchQueryRequestBuilder().resourcePath(resourcePath); + } + + @Override + public String toString() { + return new StringBuilder("Batch Query Request{ ") + .append(resourcePath) + .append(", headers=").append(headers) + .append(", queryParams=").append(queryParams) + .append('}') + .toString(); + } + + public static class Olingo2BatchQueryRequestBuilder { + private Olingo2BatchQueryRequest request = new Olingo2BatchQueryRequest(); + + public Olingo2BatchQueryRequest build() { + return request; + } + + public Olingo2BatchQueryRequestBuilder resourcePath(String resourcePath) { + request.resourcePath = resourcePath; + return this; + } + + public Olingo2BatchQueryRequestBuilder headers(Map<String, String> headers) { + request.headers = headers; + return this; + } + + public Olingo2BatchQueryRequestBuilder queryParams(Map<String, String> queryParams) { + queryParams = queryParams; + return this; + } + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/9a92064c/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchRequest.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchRequest.java b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchRequest.java new file mode 100644 index 0000000..1da4ee2 --- /dev/null +++ b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchRequest.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.olingo2.api.batch; + +import java.util.HashMap; +import java.util.Map; + +/** + * Base part in a multipart Batch request. + */ +public abstract class Olingo2BatchRequest { + + protected String resourcePath; + protected Map<String, String> headers = new HashMap<String, String>(); + + public String getResourcePath() { + return resourcePath; + } + + public Map<String, String> getHeaders() { + return headers; + } + + public abstract String toString(); +} http://git-wip-us.apache.org/repos/asf/camel/blob/9a92064c/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchResponse.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchResponse.java b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchResponse.java new file mode 100644 index 0000000..830ac46 --- /dev/null +++ b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Olingo2BatchResponse.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.camel.component.olingo2.api.batch; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Batch Response part. + */ +public class Olingo2BatchResponse { + + private final int statusCode; + private final String statusInfo; + + private final String contentId; + + private final Map<String, String> headers; + private final Object body; + + + public Olingo2BatchResponse(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/9a92064c/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Operation.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Operation.java b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Operation.java new file mode 100644 index 0000000..c08df52 --- /dev/null +++ b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/batch/Operation.java @@ -0,0 +1,35 @@ +/** + * 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.olingo2.api.batch; + +/** +* OData operation used by {@link org.apache.camel.component.olingo2.api.batch.Olingo2BatchChangeRequest}. +*/ +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/9a92064c/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/AbstractFutureCallback.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/AbstractFutureCallback.java b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/AbstractFutureCallback.java new file mode 100644 index 0000000..04be21a --- /dev/null +++ b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/AbstractFutureCallback.java @@ -0,0 +1,104 @@ +/** + * 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.olingo2.api.impl; + +import java.io.IOException; + +import org.apache.camel.component.olingo2.api.Olingo2ResponseHandler; +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.odata2.api.commons.HttpStatusCodes; +import org.apache.olingo.odata2.api.ep.EntityProvider; +import org.apache.olingo.odata2.api.ep.EntityProviderException; +import org.apache.olingo.odata2.api.exception.ODataApplicationException; +import org.apache.olingo.odata2.api.exception.ODataException; +import org.apache.olingo.odata2.api.processor.ODataErrorContext; +import org.apache.olingo.odata2.core.commons.ContentType; + +/** +* Helper implementation of {@link org.apache.http.concurrent.FutureCallback} + * for {@link org.apache.camel.component.olingo2.api.impl.Olingo2AppImpl} +*/ +public abstract class AbstractFutureCallback<T> implements FutureCallback<HttpResponse> { + + private final Olingo2ResponseHandler<T> responseHandler; + + AbstractFutureCallback(Olingo2ResponseHandler<T> responseHandler) { + this.responseHandler = responseHandler; + } + + public static HttpStatusCodes checkStatus(HttpResponse response) throws ODataApplicationException { + final StatusLine statusLine = response.getStatusLine(); + HttpStatusCodes httpStatusCode = HttpStatusCodes.fromStatusCode(statusLine.getStatusCode()); + if (400 <= httpStatusCode.getStatusCode() && httpStatusCode.getStatusCode() <= 599) { + if (response.getEntity() != null) { + try { + final ContentType responseContentType = ContentType.create( + response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()); + + switch (responseContentType.getODataFormat()) { + case ATOM: + case XML: + case JSON: + final ODataErrorContext errorContext = EntityProvider.readErrorDocument( + response.getEntity().getContent(), + responseContentType.toString()); + throw new ODataApplicationException(errorContext.getMessage(), + errorContext.getLocale(), httpStatusCode, errorContext.getErrorCode(), + errorContext.getException()); + default: + // fall through to default exception with status line information + } + } catch (EntityProviderException e) { + throw new ODataApplicationException(e.getMessage(), response.getLocale(), httpStatusCode, e); + } catch (IOException e) { + throw new ODataApplicationException(e.getMessage(), response.getLocale(), httpStatusCode, e); + } + } + + throw new ODataApplicationException(statusLine.getReasonPhrase(), response.getLocale(), httpStatusCode); + } + + return httpStatusCode; + } + + @Override + public final void completed(HttpResponse result) { + try { + // check response status + checkStatus(result); + + onCompleted(result); + } catch (Exception e) { + failed(e); + } + } + + 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/9a92064c/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/HttpMerge.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/HttpMerge.java b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/HttpMerge.java new file mode 100644 index 0000000..b002b4e --- /dev/null +++ b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/HttpMerge.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.camel.component.olingo2.api.impl; + +import java.net.URI; + +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; + +/** + * HTTP MERGE method. + */ +public class HttpMerge extends HttpEntityEnclosingRequestBase { + + public static final String METHOD_NAME = "MERGE"; + + public HttpMerge() { + } + + public HttpMerge(final URI uri) { + super(); + setURI(uri); + } + + public HttpMerge(final String uri) { + super(); + setURI(URI.create(uri)); + } + + @Override + public String getMethod() { + return METHOD_NAME; + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/9a92064c/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/Olingo2AppImpl.java ---------------------------------------------------------------------- diff --git a/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/Olingo2AppImpl.java b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/Olingo2AppImpl.java new file mode 100644 index 0000000..125f7cf --- /dev/null +++ b/components/camel-olingo2/camel-olingo2-api/src/main/java/org/apache/camel/component/olingo2/api/impl/Olingo2AppImpl.java @@ -0,0 +1,901 @@ +/** + * 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.olingo2.api.impl; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; + +import org.apache.camel.component.olingo2.api.Olingo2App; +import org.apache.camel.component.olingo2.api.Olingo2ResponseHandler; +import org.apache.camel.component.olingo2.api.batch.Olingo2BatchChangeRequest; +import org.apache.camel.component.olingo2.api.batch.Olingo2BatchQueryRequest; +import org.apache.camel.component.olingo2.api.batch.Olingo2BatchRequest; +import org.apache.camel.component.olingo2.api.batch.Olingo2BatchResponse; +import org.apache.camel.component.olingo2.api.batch.Operation; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.HttpVersion; +import org.apache.http.StatusLine; +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.entity.InputStreamEntity; +import org.apache.http.entity.StringEntity; +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.BasicHttpResponse; +import org.apache.http.message.BasicStatusLine; +import org.apache.http.nio.client.util.HttpAsyncClientUtils; +import org.apache.olingo.odata2.api.ODataServiceVersion; +import org.apache.olingo.odata2.api.batch.BatchException; +import org.apache.olingo.odata2.api.client.batch.BatchChangeSet; +import org.apache.olingo.odata2.api.client.batch.BatchChangeSetPart; +import org.apache.olingo.odata2.api.client.batch.BatchPart; +import org.apache.olingo.odata2.api.client.batch.BatchQueryPart; +import org.apache.olingo.odata2.api.client.batch.BatchSingleResponse; +import org.apache.olingo.odata2.api.commons.HttpStatusCodes; +import org.apache.olingo.odata2.api.commons.ODataHttpHeaders; +import org.apache.olingo.odata2.api.edm.Edm; +import org.apache.olingo.odata2.api.edm.EdmEntitySet; +import org.apache.olingo.odata2.api.edm.EdmException; +import org.apache.olingo.odata2.api.edm.EdmProperty; +import org.apache.olingo.odata2.api.ep.EntityProvider; +import org.apache.olingo.odata2.api.ep.EntityProviderException; +import org.apache.olingo.odata2.api.ep.EntityProviderReadProperties; +import org.apache.olingo.odata2.api.ep.EntityProviderWriteProperties; +import org.apache.olingo.odata2.api.exception.ODataApplicationException; +import org.apache.olingo.odata2.api.exception.ODataException; +import org.apache.olingo.odata2.api.processor.ODataResponse; +import org.apache.olingo.odata2.api.uri.PathSegment; +import org.apache.olingo.odata2.api.uri.UriParser; +import org.apache.olingo.odata2.core.ODataPathSegmentImpl; +import org.apache.olingo.odata2.core.commons.ContentType; +import org.apache.olingo.odata2.core.uri.UriInfoImpl; + +/** + * Application API used by Olingo2 Component. + */ +public final class Olingo2AppImpl implements Olingo2App { + + public static final ContentType APPLICATION_FORM_URL_ENCODED = ContentType.create("application", "x-www-form-urlencoded"); + + public static final String METADATA = "$metadata"; + + private static final String SEPARATOR = "/"; + + private static final String BOUNDARY_PREFIX = "batch_"; + private static final String BOUNDARY_PARAMETER = "boundary"; + + private static final ContentType METADATA_CONTENT_TYPE = ContentType.APPLICATION_XML_CS_UTF_8; + private static final ContentType SERVICE_DOCUMENT_CONTENT_TYPE = ContentType.APPLICATION_ATOM_SVC_CS_UTF_8; + private static final ContentType BATCH_CONTENT_TYPE = + ContentType.MULTIPART_MIXED.receiveWithCharsetParameter(ContentType.CHARSET_UTF_8); + private static final String BATCH = "$batch"; + private static final String MAX_DATA_SERVICE_VERSION = "Max" + ODataHttpHeaders.DATASERVICEVERSION; + + private final CloseableHttpAsyncClient client; + + private String serviceUri; + private ContentType contentType; + private Map<String, String> httpHeaders; + + /** + * Create Olingo2 Application with default HTTP configuration. + */ + public Olingo2AppImpl(String serviceUri) { + this(serviceUri, null); + } + + /** + * Create Olingo2 Application with custom HTTP client builder. + * + * @param serviceUri Service Application base URI. + * @param builder custom HTTP client builder. + */ + public Olingo2AppImpl(String serviceUri, HttpAsyncClientBuilder builder) { + if (serviceUri == null) { + throw new IllegalArgumentException("serviceUri"); + } + this.serviceUri = serviceUri; + + if (builder == null) { + this.client = HttpAsyncClients.createDefault(); + } else { + this.client = builder.build(); + } + this.client.start(); + this.contentType = ContentType.APPLICATION_JSON_CS_UTF_8; + } + + @Override + public void setServiceUri(String serviceUri) { + this.serviceUri = 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.toString(); + } + + @Override + public void setContentType(String contentType) { + this.contentType = ContentType.parse(contentType); + } + + @Override + public void close() { + HttpAsyncClientUtils.closeQuietly(client); + } + + @Override + public <T> void read(final Edm edm, final String resourcePath, final Map<String, String> queryParams, + final Olingo2ResponseHandler<T> responseHandler) { + + final UriInfoImpl uriInfo = parseUri(edm, resourcePath, queryParams); + + execute(new HttpGet(createUri(resourcePath, queryParams)), getResourceContentType(uriInfo), + new AbstractFutureCallback<T>(responseHandler) { + + @Override + @SuppressWarnings("unchecked") + public void onCompleted(HttpResponse result) throws IOException { + + readContent(uriInfo, result.getEntity() != null ? result.getEntity().getContent() : null, + responseHandler); + } + + }); + } + + private ContentType getResourceContentType(UriInfoImpl uriInfo) { + ContentType resourceContentType; + switch (uriInfo.getUriType()) { + case URI0: + // service document + resourceContentType = SERVICE_DOCUMENT_CONTENT_TYPE; + break; + case URI8: + // metadata + resourceContentType = METADATA_CONTENT_TYPE; + break; + case URI4: + case URI5: + // is it a $value URI?? + if (uriInfo.isValue()) { + // property value and $count + resourceContentType = ContentType.TEXT_PLAIN_CS_UTF_8; + } else { + resourceContentType = contentType; + } + break; + case URI15: + case URI16: + case URI50A: + case URI50B: + // $count + resourceContentType = ContentType.TEXT_PLAIN_CS_UTF_8; + break; + default: + resourceContentType = contentType; + } + return resourceContentType; + } + + @Override + public <T> void create(Edm edm, String resourcePath, Object data, Olingo2ResponseHandler<T> responseHandler) { + final UriInfoImpl 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, Olingo2ResponseHandler<T> responseHandler) { + final UriInfoImpl uriInfo = parseUri(edm, resourcePath, null); + + writeContent(edm, new HttpPut(createUri(resourcePath, null)), uriInfo, data, responseHandler); + } + + @Override + public <T> void patch(Edm edm, String resourcePath, Object data, Olingo2ResponseHandler<T> responseHandler) { + final UriInfoImpl 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, Olingo2ResponseHandler<T> responseHandler) { + final UriInfoImpl uriInfo = parseUri(edm, resourcePath, null); + + writeContent(edm, new HttpMerge(createUri(resourcePath, null)), uriInfo, data, responseHandler); + } + + @Override + public void batch(Edm edm, Object data, Olingo2ResponseHandler<List<Olingo2BatchResponse>> responseHandler) { + final UriInfoImpl uriInfo = parseUri(edm, BATCH, null); + + writeContent(edm, new HttpPost(createUri(BATCH, null)), uriInfo, data, responseHandler); + } + + @Override + public void delete(String resourcePath, final Olingo2ResponseHandler<HttpStatusCodes> responseHandler) { + + execute(new HttpDelete(createUri(resourcePath)), contentType, + new AbstractFutureCallback<HttpStatusCodes>(responseHandler) { + @Override + public void onCompleted(HttpResponse result) { + final StatusLine statusLine = result.getStatusLine(); + responseHandler.onResponse(HttpStatusCodes.fromStatusCode(statusLine.getStatusCode())); + } + }); + } + + private <T> void readContent(UriInfoImpl uriInfo, InputStream content, Olingo2ResponseHandler<T> responseHandler) { + try { + responseHandler.onResponse(this.<T>readContent(uriInfo, content)); + } catch (EntityProviderException e) { + responseHandler.onException(e); + } catch (ODataApplicationException e) { + responseHandler.onException(e); + } + } + + @SuppressWarnings("unchecked") + private <T> T readContent(UriInfoImpl uriInfo, InputStream content) + throws EntityProviderException, ODataApplicationException { + T response; + switch (uriInfo.getUriType()) { + case URI0: + // service document + response = (T) EntityProvider.readServiceDocument(content, SERVICE_DOCUMENT_CONTENT_TYPE.toString()); + break; + + case URI8: + // $metadata + response = (T) EntityProvider.readMetadata(content, false); + break; + + case URI7A: + // link + response = (T) EntityProvider.readLink(getContentType(), uriInfo.getTargetEntitySet(), content); + break; + + case URI7B: + // links + response = (T) EntityProvider.readLinks(getContentType(), uriInfo.getTargetEntitySet(), content); + break; + + case URI3: + // complex property + final List<EdmProperty> complexPropertyPath = uriInfo.getPropertyPath(); + final EdmProperty complexProperty = complexPropertyPath.get(complexPropertyPath.size() - 1); + response = (T) EntityProvider.readProperty(getContentType(), + complexProperty, content, EntityProviderReadProperties.init().build()); + break; + + case URI4: + case URI5: + // simple property + final List<EdmProperty> simplePropertyPath = uriInfo.getPropertyPath(); + final EdmProperty simpleProperty = simplePropertyPath.get(simplePropertyPath.size() - 1); + if (uriInfo.isValue()) { + response = (T) EntityProvider.readPropertyValue(simpleProperty, content); + } else { + response = (T) EntityProvider.readProperty(getContentType(), + simpleProperty, content, EntityProviderReadProperties.init().build()); + } + break; + + case URI15: + case URI16: + case URI50A: + case URI50B: + // $count + try { + final String stringCount = new String(EntityProvider.readBinary(content), ContentType.CHARSET_UTF_8); + response = (T) Long.valueOf(stringCount); + } catch (UnsupportedEncodingException e) { + throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED, e); + } + break; + + case URI1: + case URI6B: + if (uriInfo.getCustomQueryOptions().containsKey("!deltaToken")) { + // ODataDeltaFeed + response = (T) EntityProvider.readDeltaFeed( + getContentType(), + uriInfo.getTargetEntitySet(), content, + EntityProviderReadProperties.init().build()); + } else { + // ODataFeed + response = (T) EntityProvider.readFeed( + getContentType(), + uriInfo.getTargetEntitySet(), content, + EntityProviderReadProperties.init().build()); + } + break; + + case URI2: + case URI6A: + response = (T) EntityProvider.readEntry( + getContentType(), + uriInfo.getTargetEntitySet(), + content, + EntityProviderReadProperties.init().build()); + break; + + default: + throw new ODataApplicationException("Unsupported resource type " + uriInfo.getTargetType(), + Locale.ENGLISH); + } + + return response; + } + + private <T> void writeContent(final Edm edm, HttpEntityEnclosingRequestBase httpEntityRequest, + final UriInfoImpl uriInfo, final Object content, + final Olingo2ResponseHandler<T> responseHandler) { + + try { + // process resource by UriType + final ODataResponse response = writeContent(edm, uriInfo, content); + + // copy all response headers + for (String header : response.getHeaderNames()) { + httpEntityRequest.setHeader(header, response.getHeader(header)); + } + + // get (http) entity which is for default Olingo2 implementation an InputStream + if (response.getEntity() instanceof InputStream) { + httpEntityRequest.setEntity(new InputStreamEntity((InputStream) response.getEntity())); +/* + // avoid sending it without a header field set + if (!httpEntityRequest.containsHeader(HttpHeaders.CONTENT_TYPE)) { + httpEntityRequest.addHeader(HttpHeaders.CONTENT_TYPE, getContentType()); + } +*/ + } + + // execute HTTP request + execute(httpEntityRequest, contentType, new AbstractFutureCallback<T>(responseHandler) { + @SuppressWarnings("unchecked") + @Override + public void onCompleted(HttpResponse result) + throws IOException, EntityProviderException, BatchException, ODataApplicationException { + + // if a entity is created (via POST request) the response body contains the new created entity + HttpStatusCodes statusCode = HttpStatusCodes.fromStatusCode(result.getStatusLine().getStatusCode()); + if (statusCode != HttpStatusCodes.NO_CONTENT) { + + // TODO do we need to handle response based on other UriTypes??? + switch (uriInfo.getUriType()) { + case URI9: + // $batch + final List<BatchSingleResponse> singleResponses = EntityProvider.parseBatchResponse( + result.getEntity().getContent(), + result.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()); + + // parse batch response bodies + final List<Olingo2BatchResponse> responses = new ArrayList<Olingo2BatchResponse>(); + Map<String, String> contentIdLocationMap = new HashMap<String, String>(); + + final List<Olingo2BatchRequest> batchRequests = (List<Olingo2BatchRequest>) content; + final Iterator<Olingo2BatchRequest> iterator = batchRequests.iterator(); + + for (BatchSingleResponse response : singleResponses) { + final Olingo2BatchRequest request = iterator.next(); + + if (request instanceof Olingo2BatchChangeRequest + && ((Olingo2BatchChangeRequest)request).getContentId() != null) { + + contentIdLocationMap.put("$" + ((Olingo2BatchChangeRequest)request).getContentId(), + response.getHeader(HttpHeaders.LOCATION)); + } + + try { + responses.add(parseResponse(edm, contentIdLocationMap, request, response)); + } catch (Exception e) { + // report any parsing errors as error response + responses.add(new Olingo2BatchResponse( + Integer.parseInt(response.getStatusCode()), + response.getStatusInfo(), response.getContentId(), response.getHeaders(), + new ODataApplicationException( + "Error parsing response for " + request + ": " + e.getMessage(), + Locale.ENGLISH, e))); + } + } + responseHandler.onResponse((T) responses); + break; + + default: + // get the response content as an ODataEntry object + responseHandler.onResponse((T) EntityProvider.readEntry(response.getContentHeader(), + uriInfo.getTargetEntitySet(), + result.getEntity().getContent(), + EntityProviderReadProperties.init().build())); + break; + } + + } else { + responseHandler.onResponse( + (T) HttpStatusCodes.fromStatusCode(result.getStatusLine().getStatusCode())); + } + } + }); + } catch (ODataException e) { + responseHandler.onException(e); + } catch (URISyntaxException e) { + responseHandler.onException(e); + } catch (UnsupportedEncodingException e) { + responseHandler.onException(e); + } catch (IOException e) { + responseHandler.onException(e); + } + } + + private ODataResponse writeContent(Edm edm, UriInfoImpl uriInfo, Object content) + throws ODataApplicationException, EdmException, EntityProviderException, URISyntaxException, IOException { + + String responseContentType = getContentType(); + ODataResponse response; + + switch (uriInfo.getUriType()) { + case URI4: + case URI5: + // simple property + final List<EdmProperty> simplePropertyPath = uriInfo.getPropertyPath(); + final EdmProperty simpleProperty = simplePropertyPath.get(simplePropertyPath.size() - 1); + responseContentType = simpleProperty.getMimeType(); + if (uriInfo.isValue()) { + response = EntityProvider.writePropertyValue(simpleProperty, content); + responseContentType = ContentType.TEXT_PLAIN_CS_UTF_8.toString(); + } else { + response = EntityProvider.writeProperty(getContentType(), simpleProperty, content); + } + break; + + case URI3: + // complex property + final List<EdmProperty> complexPropertyPath = uriInfo.getPropertyPath(); + final EdmProperty complexProperty = complexPropertyPath.get(complexPropertyPath.size() - 1); + response = EntityProvider.writeProperty(responseContentType, complexProperty, content); + break; + + case URI7A: + // $links with 0..1 cardinality property + final EdmEntitySet targetLinkEntitySet = uriInfo.getTargetEntitySet(); + final URI rootLinkUri = new URI(targetLinkEntitySet.getName()); + EntityProviderWriteProperties linkProperties = + EntityProviderWriteProperties.serviceRoot(rootLinkUri).build(); + @SuppressWarnings("unchecked") + final Map<String, Object> linkMap = (Map<String, Object>) content; + response = EntityProvider.writeLink(responseContentType, targetLinkEntitySet, linkMap, linkProperties); + break; + + case URI7B: + // $links with * cardinality property + final EdmEntitySet targetLinksEntitySet = uriInfo.getTargetEntitySet(); + final URI rootLinksUri = new URI(targetLinksEntitySet.getName()); + EntityProviderWriteProperties linksProperties = + EntityProviderWriteProperties.serviceRoot(rootLinksUri).build(); + @SuppressWarnings("unchecked") + final Map<String, Object> linksMap = (Map<String, Object>) content; + response = EntityProvider.writeLink(responseContentType, targetLinksEntitySet, linksMap, linksProperties); + break; + + case URI1: + case URI2: + case URI6A: + case URI6B: + // Entity + final EdmEntitySet targetEntitySet = uriInfo.getTargetEntitySet(); + final URI rootUri = new URI(targetEntitySet.getName()); + EntityProviderWriteProperties properties = EntityProviderWriteProperties.serviceRoot(rootUri).build(); + @SuppressWarnings("unchecked") + final Map<String, Object> objectMap = (Map<String, Object>) content; + response = EntityProvider.writeEntry(responseContentType, targetEntitySet, objectMap, properties); + break; + + case URI9: + // $batch + @SuppressWarnings("unchecked") + final List<Olingo2BatchRequest> batchParts = (List<Olingo2BatchRequest>) content; + response = parseBatchRequest(edm, batchParts); + break; + + default: + // notify exception and return!!! + throw new ODataApplicationException("Unsupported resource type " + uriInfo.getTargetType(), + Locale.ENGLISH); + } + + return response.getContentHeader() != null ? response + : ODataResponse.fromResponse(response).contentHeader(responseContentType).build(); + } + + private ODataResponse parseBatchRequest(final Edm edm, final List<Olingo2BatchRequest> batchParts) + throws IOException, EntityProviderException, ODataApplicationException, EdmException, URISyntaxException { + + // create Batch request from parts + final ArrayList<BatchPart> parts = new ArrayList<BatchPart>(); + final ArrayList<BatchChangeSetPart> changeSetParts = new ArrayList<BatchChangeSetPart>(); + + final Map<String, String> contentIdMap = new HashMap<String, String>(); + + for (Olingo2BatchRequest batchPart : batchParts) { + + if (batchPart instanceof Olingo2BatchQueryRequest) { + + // need to add change set parts collected so far?? + if (!changeSetParts.isEmpty()) { + addChangeSetParts(parts, changeSetParts); + changeSetParts.clear(); + contentIdMap.clear(); + } + + // add to request parts + final UriInfoImpl uriInfo = parseUri(edm, batchPart.getResourcePath(), null); + parts.add(createBatchQueryPart(uriInfo, (Olingo2BatchQueryRequest) batchPart)); + + } else { + + // add to change set parts + final BatchChangeSetPart changeSetPart = createBatchChangeSetPart( + edm, contentIdMap, (Olingo2BatchChangeRequest) batchPart); + changeSetParts.add(changeSetPart); + } + } + + // add any remaining change set parts + if (!changeSetParts.isEmpty()) { + addChangeSetParts(parts, changeSetParts); + } + + final String boundary = BOUNDARY_PREFIX + UUID.randomUUID(); + InputStream batchRequest = EntityProvider.writeBatchRequest(parts, boundary); + // add two blank lines before all --batch boundaries + // otherwise Olingo2 EntityProvider parser barfs in the server!!! + final byte[] bytes = EntityProvider.readBinary(batchRequest); + final String batchRequestBody = new String(bytes, ContentType.CHARSET_UTF_8); + batchRequest = new ByteArrayInputStream(batchRequestBody.replaceAll( + "--(batch_)", "\r\n\r\n--$1").getBytes(ContentType.CHARSET_UTF_8)); + + final String contentHeader = ContentType.create(BATCH_CONTENT_TYPE, BOUNDARY_PARAMETER, boundary).toString(); + return ODataResponse.entity(batchRequest).contentHeader(contentHeader).build(); + } + + private void addChangeSetParts(ArrayList<BatchPart> parts, ArrayList<BatchChangeSetPart> changeSetParts) { + final BatchChangeSet changeSet = BatchChangeSet.newBuilder().build(); + for (BatchChangeSetPart changeSetPart : changeSetParts) { + changeSet.add(changeSetPart); + } + parts.add(changeSet); + } + + private BatchChangeSetPart createBatchChangeSetPart(Edm edm, Map<String, String> contentIdMap, + Olingo2BatchChangeRequest batchRequest) + throws EdmException, URISyntaxException, EntityProviderException, IOException, ODataApplicationException { + + // build body string + String resourcePath = batchRequest.getResourcePath(); + // is it a referenced entity? + if (resourcePath.startsWith("$")) { + resourcePath = replaceContentId(edm, resourcePath, contentIdMap); + } + + final UriInfoImpl uriInfo = parseUri(edm, resourcePath, null); + + // serialize data into ODataResponse object, if set in request and this is not a DELETE request + final Map<String, String> headers = new HashMap<String, String>(); + byte[] body = null; + + if (batchRequest.getBody() != null + && !Operation.DELETE.equals(batchRequest.getOperation())) { + + final ODataResponse response = writeContent(edm, uriInfo, batchRequest.getBody()); + // copy response headers + for (String header : response.getHeaderNames()) { + headers.put(header, response.getHeader(header)); + } + + // get (http) entity which is for default Olingo2 implementation an InputStream + body = response.getEntity() instanceof InputStream + ? EntityProvider.readBinary((InputStream) response.getEntity()) : null; + if (body != null) { + headers.put(HttpHeaders.CONTENT_LENGTH, String.valueOf(body.length)); + } + } + + headers.put(HttpHeaders.ACCEPT, getResourceContentType(uriInfo).toString()); + if (!headers.containsKey(HttpHeaders.CONTENT_TYPE)) { + headers.put(HttpHeaders.CONTENT_TYPE, getContentType()); + } + + // add request headers + headers.putAll(batchRequest.getHeaders()); + + final String contentId = batchRequest.getContentId(); + if (contentId != null) { + contentIdMap.put("$" + contentId, resourcePath); + } + return BatchChangeSetPart.uri(createBatchUri(batchRequest)) + .method(batchRequest.getOperation().getHttpMethod()) + .contentId(contentId) + .headers(headers) + .body(body == null ? null : new String(body, ContentType.CHARSET_UTF_8)).build(); + } + + private BatchQueryPart createBatchQueryPart(UriInfoImpl uriInfo, Olingo2BatchQueryRequest batchRequest) { + + final Map<String, String> headers = new HashMap<String, String>(batchRequest.getHeaders()); + if (!headers.containsKey(HttpHeaders.ACCEPT)) { + headers.put(HttpHeaders.ACCEPT, getResourceContentType(uriInfo).toString()); + } + + return BatchQueryPart.method("GET") + .uri(createBatchUri(batchRequest)) + .headers(headers) + .build(); + } + + private static String replaceContentId(Edm edm, String entityReference, Map<String, String> contentIdMap) throws EdmException { + final int pathSeparator = entityReference.indexOf('/'); + final StringBuilder referencedEntity; + if (pathSeparator == -1) { + referencedEntity = new StringBuilder(contentIdMap.get(entityReference)); + } else { + referencedEntity = new StringBuilder(contentIdMap.get(entityReference.substring(0, pathSeparator))); + } + + // create a dummy entity location by adding a dummy key predicate + final EdmEntitySet entitySet = edm.getDefaultEntityContainer().getEntitySet(referencedEntity.toString()); + final List<EdmProperty> keyProperties = entitySet.getEntityType().getKeyProperties(); + + if (keyProperties.size() == 1) { + referencedEntity.append("('dummy')"); + } else { + referencedEntity.append("("); + for (EdmProperty keyProperty : keyProperties) { + referencedEntity.append(keyProperty.getName()).append('=').append("'dummy',"); + } + referencedEntity.deleteCharAt(referencedEntity.length() - 1); + referencedEntity.append(')'); + } + + return pathSeparator == -1 ? referencedEntity.toString() + : referencedEntity.append(entityReference.substring(pathSeparator)).toString(); + } + + private Olingo2BatchResponse parseResponse(Edm edm, Map<String, String> contentIdLocationMap, + Olingo2BatchRequest request, BatchSingleResponse response) + throws EntityProviderException, ODataApplicationException { + + // validate HTTP status + final int statusCode = Integer.parseInt(response.getStatusCode()); + final String statusInfo = response.getStatusInfo(); + + final BasicHttpResponse httpResponse = new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, + statusCode, statusInfo)); + final Map<String, String> headers = response.getHeaders(); + for (Map.Entry<String, String> entry : headers.entrySet()) { + httpResponse.setHeader(entry.getKey(), entry.getValue()); + } + + ByteArrayInputStream content = null; + try { + if (response.getBody() != null) { + final ContentType partContentType = ContentType.create( + headers.get(HttpHeaders.CONTENT_TYPE)).receiveWithCharsetParameter(ContentType.CHARSET_UTF_8); + final String charset = partContentType.getParameters().get(ContentType.PARAMETER_CHARSET); + + final String body = response.getBody(); + content = body != null ? new ByteArrayInputStream(body.getBytes(charset)) : null; + + httpResponse.setEntity(new StringEntity(body, charset)); + } + + AbstractFutureCallback.checkStatus(httpResponse); + } catch (ODataApplicationException e) { + return new Olingo2BatchResponse( + statusCode, statusInfo, response.getContentId(), + response.getHeaders(), e); + } catch (UnsupportedEncodingException e) { + return new Olingo2BatchResponse( + statusCode, statusInfo, response.getContentId(), + response.getHeaders(), e); + } + + // resolve resource path and query params and parse batch part uri + final String resourcePath = request.getResourcePath(); + final String resolvedResourcePath; + if (resourcePath.startsWith("$") && !(METADATA.equals(resourcePath) || BATCH.equals(resourcePath))) { + resolvedResourcePath = findLocation(resourcePath, contentIdLocationMap); + } else { + final String resourceLocation = response.getHeader(HttpHeaders.LOCATION); + resolvedResourcePath = resourceLocation != null + ? resourceLocation.substring(serviceUri.length()) : resourcePath; + } + final Map<String, String> resolvedQueryParams = request instanceof Olingo2BatchQueryRequest + ? ((Olingo2BatchQueryRequest) request).getQueryParams() : null; + final UriInfoImpl uriInfo = parseUri(edm, resolvedResourcePath, resolvedQueryParams); + + // resolve response content + final Object resolvedContent = content != null ? readContent(uriInfo, content) : null; + + return new Olingo2BatchResponse(statusCode, statusInfo, response.getContentId(), response.getHeaders(), + resolvedContent); + } + + private String findLocation(String resourcePath, Map<String, String> contentIdLocationMap) { + final int pathSeparator = resourcePath.indexOf('/'); + if (pathSeparator == -1) { + return contentIdLocationMap.get(resourcePath); + } else { + return contentIdLocationMap.get(resourcePath.substring(0, pathSeparator)) + + resourcePath.substring(pathSeparator); + } + + } + + private String createBatchUri(Olingo2BatchRequest part) { + String result; + if (part instanceof Olingo2BatchQueryRequest) { + final Olingo2BatchQueryRequest queryPart = (Olingo2BatchQueryRequest) part; + result = createUri(queryPart.getResourcePath(), queryPart.getQueryParams()); + } else { + result = createUri(part.getResourcePath()); + } + // strip base URI + return result.substring(serviceUri.length() + 1); + } + + private String createUri(String resourcePath) { + return createUri(resourcePath, null); + } + + private String createUri(String resourcePath, Map<String, String> queryParams) { + + final StringBuilder absolutUri = new StringBuilder(serviceUri).append(SEPARATOR).append(resourcePath); + if (queryParams != null && !queryParams.isEmpty()) { + absolutUri.append("/?"); + int nParams = queryParams.size(); + int index = 0; + for (Map.Entry<String, String> entry : queryParams.entrySet()) { + absolutUri.append(entry.getKey()).append('=').append(entry.getValue()); + if (++index < nParams) { + absolutUri.append('&'); + } + } + } + return absolutUri.toString(); + } + + private static UriInfoImpl parseUri(Edm edm, String resourcePath, Map<String, String> queryParams) { + UriInfoImpl result; + try { + final List<PathSegment> pathSegments = new ArrayList<PathSegment>(); + final String[] segments = new URI(resourcePath).getPath().split(SEPARATOR); + if (queryParams == null) { + queryParams = Collections.emptyMap(); + } + for (String segment : segments) { + if (segment.indexOf(';') == -1) { + + pathSegments.add(new ODataPathSegmentImpl(segment, null)); + } else { + + // handle matrix params in path segment + final String[] splitSegment = segment.split(";"); + segment = splitSegment[0]; + + Map<String, List<String>> matrixParams = new HashMap<String, List<String>>(); + for (int i = 1; i < splitSegment.length; i++) { + final String[] param = splitSegment[i].split("="); + List<String> values = matrixParams.get(param[0]); + if (values == null) { + values = new ArrayList<String>(); + matrixParams.put(param[0], values); + } + if (param[1].indexOf(',') == -1) { + values.add(param[1]); + } else { + values.addAll(Arrays.asList(param[1].split(","))); + } + } + pathSegments.add(new ODataPathSegmentImpl(segment, matrixParams)); + } + } + result = (UriInfoImpl) UriParser.parse(edm, pathSegments, queryParams); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("resourcePath: " + e.getMessage(), e); + } catch (ODataException e) { + throw new IllegalArgumentException("resourcePath: " + e.getMessage(), e); + } + + return result; + } + + // public for unit test, not to be used otherwise + 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 (!APPLICATION_FORM_URL_ENCODED.equals(contentType) + && !contentType.getType().equals(ContentType.MULTIPART_MIXED.getType())) { + // otherwise accept what is being sent + httpUriRequest.addHeader(HttpHeaders.ACCEPT, contentTypeString); + } + // is something being sent? + if (httpUriRequest instanceof HttpEntityEnclosingRequestBase) { + 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 client protocol version if not specified + if (!httpUriRequest.containsHeader(ODataHttpHeaders.DATASERVICEVERSION)) { + httpUriRequest.addHeader(ODataHttpHeaders.DATASERVICEVERSION, ODataServiceVersion.V20); + } + if (!httpUriRequest.containsHeader(MAX_DATA_SERVICE_VERSION)) { + httpUriRequest.addHeader(MAX_DATA_SERVICE_VERSION, ODataServiceVersion.V30); + } + + // execute request + client.execute(httpUriRequest, callback); + } + +}