You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ac...@apache.org on 2019/08/23 08:51:10 UTC
[camel] 01/03: CAMEL-13598: Implement ETag support in olingo
components
This is an automated email from the ASF dual-hosted git repository.
acosentino pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel.git
commit fe821aee1ea761ad5841b081958efe7ccadf39a4
Author: phantomjinx <p....@phantomjinx.co.uk>
AuthorDate: Fri Aug 16 15:56:02 2019 +0100
CAMEL-13598: Implement ETag support in olingo components
* Supports odata services that implement concurrency properties on entities
by ensuring that when performing patch, update & delete operations, the
OlingoApp first reads the requisite entity and retrieves its ETag. This
is then added into the subsequent request under the If-Match header
* Adds additional tests for specifically testing the ETag functionality.
---
.../component/olingo2/api/impl/Olingo2AppImpl.java | 119 +++++++++-
.../camel-olingo2/camel-olingo2-component/pom.xml | 11 +
.../olingo2/AbstractOlingo2AppAPITestSupport.java | 192 ++++++++++++++++
.../olingo2/Olingo2AppAPIETagEnabledTest.java | 243 +++++++++++++++++++++
.../camel/component/olingo2/Olingo2AppAPITest.java | 175 +--------------
.../component/olingo2/etag-enabled-service.xml | 26 +++
.../component/olingo4/api/impl/Olingo4AppImpl.java | 126 ++++++++++-
.../camel/component/olingo4/Olingo4AppAPITest.java | 137 +++++++++++-
.../olingo4/Olingo4ComponentProducerTest.java | 6 +-
9 files changed, 842 insertions(+), 193 deletions(-)
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
index 19bc581..e5fae97 100644
--- 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
@@ -34,8 +34,9 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import java.util.UUID;
-
+import java.util.function.Consumer;
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;
@@ -57,6 +58,7 @@ 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.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.entity.ContentType;
@@ -88,6 +90,8 @@ 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.ep.entry.EntryMetadata;
+import org.apache.olingo.odata2.api.ep.entry.ODataEntry;
import org.apache.olingo.odata2.api.exception.ODataApplicationException;
import org.apache.olingo.odata2.api.exception.ODataException;
import org.apache.olingo.odata2.api.processor.ODataResponse;
@@ -320,7 +324,10 @@ public final class Olingo2AppImpl implements Olingo2App {
final Olingo2ResponseHandler<T> responseHandler) {
final UriInfoWithType uriInfo = parseUri(edm, resourcePath, null);
- writeContent(edm, new HttpPut(createUri(resourcePath, null)), uriInfo, endpointHttpHeaders, data, responseHandler);
+ augmentWithETag(edm, resourcePath, endpointHttpHeaders,
+ new HttpPut(createUri(resourcePath, null)),
+ request -> writeContent(edm, (HttpPut) request, uriInfo, endpointHttpHeaders, data, responseHandler),
+ responseHandler);
}
@Override
@@ -331,7 +338,10 @@ public final class Olingo2AppImpl implements Olingo2App {
final Olingo2ResponseHandler<T> responseHandler) {
final UriInfoWithType uriInfo = parseUri(edm, resourcePath, null);
- writeContent(edm, new HttpPatch(createUri(resourcePath, null)), uriInfo, endpointHttpHeaders, data, responseHandler);
+ augmentWithETag(edm, resourcePath, endpointHttpHeaders,
+ new HttpPatch(createUri(resourcePath, null)),
+ request -> writeContent(edm, (HttpPatch) request, uriInfo, endpointHttpHeaders, data, responseHandler),
+ responseHandler);
}
@Override
@@ -342,7 +352,10 @@ public final class Olingo2AppImpl implements Olingo2App {
final Olingo2ResponseHandler<T> responseHandler) {
final UriInfoWithType uriInfo = parseUri(edm, resourcePath, null);
- writeContent(edm, new HttpMerge(createUri(resourcePath, null)), uriInfo, endpointHttpHeaders, data, responseHandler);
+ augmentWithETag(edm, resourcePath, endpointHttpHeaders,
+ new HttpMerge(createUri(resourcePath, null)),
+ request -> writeContent(edm, (HttpMerge) request, uriInfo, endpointHttpHeaders, data, responseHandler),
+ responseHandler);
}
@Override
@@ -359,9 +372,11 @@ public final class Olingo2AppImpl implements Olingo2App {
public void delete(final String resourcePath,
final Map<String, String> endpointHttpHeaders,
final Olingo2ResponseHandler<HttpStatusCodes> responseHandler) {
+ HttpDelete deleteRequest = new HttpDelete(createUri(resourcePath));
- execute(new HttpDelete(createUri(resourcePath)), contentType,
- endpointHttpHeaders, new AbstractFutureCallback<HttpStatusCodes>(responseHandler) {
+ Consumer<HttpRequestBase> deleteFunction = (request) -> {
+ execute(request, contentType,
+ endpointHttpHeaders, new AbstractFutureCallback<HttpStatusCodes>(responseHandler) {
@Override
public void onCompleted(HttpResponse result) {
final StatusLine statusLine = result.getStatusLine();
@@ -369,6 +384,98 @@ public final class Olingo2AppImpl implements Olingo2App {
headersToMap(result.getAllHeaders()));
}
});
+ };
+
+ augmentWithETag(null, resourcePath, endpointHttpHeaders,
+ deleteRequest,
+ deleteFunction,
+ responseHandler);
+ }
+
+ /**
+ * On occasion, some resources are protected with Optimistic Concurrency via the use of eTags.
+ * This will first conduct a read on the given entity resource, find its eTag then perform the given
+ * delegate request function, augmenting the request with the eTag, if appropriate.
+ *
+ * Since read operations may be asynchronous, it is necessary to chain together the methods via
+ * the use of a {@link Consumer} function. Only when the response from the read returns will
+ * this delegate function be executed.
+ *
+ * @param edm the Edm object to be interrogated
+ * @param resourcePath the resource path of the entity to be operated on
+ * @param endpointHttpHeaders the headers provided from the endpoint which may be required for the read operation
+ * @param httpRequest the request to be updated, if appropriate, with the eTag and provided to the delegate request function
+ * @param delegateRequestFn the function to be invoked in response to the read operation
+ * @param delegateResponseHandler the response handler to respond if any errors occur during the read operation
+ */
+ private <T> void augmentWithETag(final Edm edm, final String resourcePath, final Map<String, String> endpointHttpHeaders,
+ final HttpRequestBase httpRequest,
+ final Consumer<HttpRequestBase> delegateRequestFn,
+ final Olingo2ResponseHandler<T> delegateResponseHandler) {
+
+ if (edm == null) {
+ // Can be the case if calling a delete then need to do a metadata call first
+ final Olingo2ResponseHandler<Edm> edmResponseHandler = new Olingo2ResponseHandler<Edm>() {
+ @Override
+ public void onResponse(Edm response, Map<String, String> responseHeaders) {
+ //
+ // Call this method again with an intact edm object
+ //
+ augmentWithETag(response, resourcePath, endpointHttpHeaders, httpRequest, delegateRequestFn, delegateResponseHandler);
+ }
+
+ @Override
+ public void onException(Exception ex) {
+ delegateResponseHandler.onException(ex);
+ }
+
+ @Override
+ public void onCanceled() {
+ delegateResponseHandler.onCanceled();
+ }
+ };
+
+ //
+ // Reads the metadata to establish an Edm object
+ // then the response handler invokes this method again with the new edm object
+ //
+ read(null, "$metadata", null, null, edmResponseHandler);
+
+ } else {
+
+ //
+ // The handler that responds to the read operation and supplies an ETag if necessary
+ // and invokes the delegate request function
+ //
+ Olingo2ResponseHandler<T> eTagReadHandler = new Olingo2ResponseHandler<T>() {
+
+ @Override
+ public void onResponse(T response, Map<String, String> responseHeaders) {
+ if (response instanceof ODataEntry) {
+ ODataEntry e = (ODataEntry) response;
+ Optional
+ .ofNullable(e.getMetadata())
+ .map(EntryMetadata::getEtag)
+ .ifPresent(v -> httpRequest.addHeader("If-Match", v));
+ }
+
+ // Invoke the delegate request function providing the modified request
+ delegateRequestFn.accept(httpRequest);
+ }
+
+ @Override
+ public void onException(Exception ex) {
+ delegateResponseHandler.onException(ex);
+ }
+
+ @Override
+ public void onCanceled() {
+ delegateResponseHandler.onCanceled();
+ }
+ };
+
+ read(edm, resourcePath, null, endpointHttpHeaders, eTagReadHandler);
+ }
}
private <T> void readContent(UriInfoWithType uriInfo, Map<String, String> responseHeaders, InputStream content, Olingo2ResponseHandler<T> responseHandler) {
diff --git a/components/camel-olingo2/camel-olingo2-component/pom.xml b/components/camel-olingo2/camel-olingo2-component/pom.xml
index 6746f68..b007e07 100644
--- a/components/camel-olingo2/camel-olingo2-component/pom.xml
+++ b/components/camel-olingo2/camel-olingo2-component/pom.xml
@@ -121,6 +121,17 @@
<artifactId>cxf-rt-frontend-jaxrs</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <version>${okclient-version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
diff --git a/components/camel-olingo2/camel-olingo2-component/src/test/java/org/apache/camel/component/olingo2/AbstractOlingo2AppAPITestSupport.java b/components/camel-olingo2/camel-olingo2-component/src/test/java/org/apache/camel/component/olingo2/AbstractOlingo2AppAPITestSupport.java
new file mode 100644
index 0000000..dc3ae9f
--- /dev/null
+++ b/components/camel-olingo2/camel-olingo2-component/src/test/java/org/apache/camel/component/olingo2/AbstractOlingo2AppAPITestSupport.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Licensed 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;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.apache.camel.component.olingo2.api.Olingo2ResponseHandler;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.http.entity.ContentType;
+import org.apache.olingo.odata2.api.ep.entry.ODataEntry;
+import org.apache.olingo.odata2.api.ep.feed.ODataDeltaFeed;
+import org.apache.olingo.odata2.api.ep.feed.ODataFeed;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AbstractOlingo2AppAPITestSupport {
+
+ protected static final String SERVICE_NAME = "MyFormula.svc";
+ protected static final Logger LOG = LoggerFactory.getLogger(Olingo2AppAPITest.class);
+ protected static final int PORT = AvailablePortFinder.getNextAvailable();
+ protected static final long TIMEOUT = 100000;
+ protected static final String MANUFACTURERS = "Manufacturers";
+ protected static final String FQN_MANUFACTURERS = "DefaultContainer.Manufacturers";
+ protected static final String ADDRESS = "Address";
+ protected static final String CARS = "Cars";
+ protected static final String TEST_KEY = "'1'";
+ protected static final String TEST_CREATE_KEY = "'123'";
+ protected static final String TEST_MANUFACTURER = FQN_MANUFACTURERS + "(" + TEST_KEY + ")";
+ protected static final String TEST_CREATE_MANUFACTURER = MANUFACTURERS + "(" + TEST_CREATE_KEY + ")";
+ protected static final String TEST_RESOURCE_CONTENT_ID = "1";
+ protected static final String TEST_RESOURCE = "$" + TEST_RESOURCE_CONTENT_ID;
+ protected static final char NEW_LINE = '\n';
+ protected static final String TEST_CAR = "Manufacturers('1')/Cars('1')";
+ protected static final String TEST_MANUFACTURER_FOUNDED_PROPERTY = "Manufacturers('1')/Founded";
+ protected static final String TEST_MANUFACTURER_FOUNDED_VALUE = "Manufacturers('1')/Founded/$value";
+ protected static final String FOUNDED_PROPERTY = "Founded";
+ protected static final String TEST_MANUFACTURER_ADDRESS_PROPERTY = "Manufacturers('1')/Address";
+ protected static final String TEST_MANUFACTURER_LINKS_CARS = "Manufacturers('1')/$links/Cars";
+ protected static final String TEST_CAR_LINK_MANUFACTURER = "Cars('1')/$links/Manufacturer";
+ protected static final String COUNT_OPTION = "/$count";
+ protected static final String TEST_SERVICE_URL = "http://localhost:" + PORT + "/" + SERVICE_NAME;
+ protected static final ContentType TEST_FORMAT = ContentType.APPLICATION_JSON;
+ protected static final String TEST_FORMAT_STRING = TEST_FORMAT.toString();
+ protected static final String ID_PROPERTY = "Id";
+
+ protected static Map<String, Object> getEntityData() {
+ Map<String, Object> data = new HashMap<>();
+ data.put(ID_PROPERTY, "123");
+ data.put("Name", "MyCarManufacturer");
+ data.put(FOUNDED_PROPERTY, new Date());
+ Map<String, Object> address = new HashMap<>();
+ address.put("Street", "Main");
+ address.put("ZipCode", "42421");
+ address.put("City", "Fairy City");
+ address.put("Country", "FarFarAway");
+ data.put(ADDRESS, address);
+ return data;
+ }
+
+ protected static void indent(StringBuilder builder, int indentLevel) {
+ for (int i = 0; i < indentLevel; i++) {
+ builder.append(" ");
+ }
+ }
+
+ protected static String prettyPrint(ODataFeed dataFeed) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("[\n");
+ for (ODataEntry entry : dataFeed.getEntries()) {
+ builder.append(prettyPrint(entry.getProperties(), 1)).append('\n');
+ }
+ builder.append("]\n");
+ return builder.toString();
+ }
+
+ protected static String prettyPrint(ODataEntry createdEntry) {
+ return prettyPrint(createdEntry.getProperties(), 0);
+ }
+
+ protected static String prettyPrint(Map<String, Object> properties, int level) {
+ StringBuilder b = new StringBuilder();
+ Set<Map.Entry<String, Object>> entries = properties.entrySet();
+
+ for (Map.Entry<String, Object> entry : entries) {
+ indent(b, level);
+ b.append(entry.getKey()).append(": ");
+ Object value = entry.getValue();
+ if (value instanceof Map) {
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> objectMap = (Map<String, Object>) value;
+ value = prettyPrint(objectMap, level + 1);
+ b.append(value).append(NEW_LINE);
+ } else if (value instanceof Calendar) {
+ Calendar cal = (Calendar) value;
+ value = DateFormat.getInstance().format(cal.getTime());
+ b.append(value).append(NEW_LINE);
+ } else if (value instanceof ODataDeltaFeed) {
+ ODataDeltaFeed feed = (ODataDeltaFeed) value;
+ List<ODataEntry> inlineEntries = feed.getEntries();
+ b.append("{");
+ for (ODataEntry oDataEntry : inlineEntries) {
+ value = prettyPrint(oDataEntry.getProperties(), level + 1);
+ b.append("\n[\n").append(value).append("\n],");
+ }
+ b.deleteCharAt(b.length() - 1);
+ indent(b, level);
+ b.append("}\n");
+ } else {
+ b.append(value).append(NEW_LINE);
+ }
+ }
+ // remove last line break
+ b.deleteCharAt(b.length() - 1);
+ return b.toString();
+ }
+
+ protected static final class TestOlingo2ResponseHandler<T> implements Olingo2ResponseHandler<T> {
+
+ private T response;
+ private Exception error;
+ private CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
+ public void onResponse(T response, Map<String, String> responseHeaders) {
+ this.response = response;
+ if (LOG.isDebugEnabled()) {
+ if (response instanceof ODataFeed) {
+ LOG.debug("Received response: {}", prettyPrint((ODataFeed) response));
+ } else if (response instanceof ODataEntry) {
+ LOG.debug("Received response: {}", prettyPrint((ODataEntry) response));
+ } else {
+ LOG.debug("Received response: {}", response);
+ }
+ }
+ latch.countDown();
+ }
+
+ @Override
+ public void onException(Exception ex) {
+ error = ex;
+ latch.countDown();
+ }
+
+ @Override
+ public void onCanceled() {
+ error = new IllegalStateException("Request Canceled");
+ latch.countDown();
+ }
+
+ public T await() throws Exception {
+ return await(TIMEOUT, TimeUnit.SECONDS);
+ }
+
+ public T await(long timeout, TimeUnit unit) throws Exception {
+ assertTrue("Timeout waiting for response", latch.await(timeout, unit));
+ if (error != null) {
+ throw error;
+ }
+ assertNotNull("Response", response);
+ return response;
+ }
+
+ public void reset() {
+ latch.countDown();
+ latch = new CountDownLatch(1);
+ response = null;
+ error = null;
+ }
+ }
+}
diff --git a/components/camel-olingo2/camel-olingo2-component/src/test/java/org/apache/camel/component/olingo2/Olingo2AppAPIETagEnabledTest.java b/components/camel-olingo2/camel-olingo2-component/src/test/java/org/apache/camel/component/olingo2/Olingo2AppAPIETagEnabledTest.java
new file mode 100644
index 0000000..9eb274a
--- /dev/null
+++ b/components/camel-olingo2/camel-olingo2-component/src/test/java/org/apache/camel/component/olingo2/Olingo2AppAPIETagEnabledTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Licensed 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import java.io.InputStream;
+import java.util.Map;
+import javax.ws.rs.HttpMethod;
+import org.apache.camel.component.olingo2.api.Olingo2App;
+import org.apache.camel.component.olingo2.api.impl.Olingo2AppImpl;
+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.EdmEntityContainer;
+import org.apache.olingo.odata2.api.edm.EdmEntitySet;
+import org.apache.olingo.odata2.api.edm.EdmEntityType;
+import org.apache.olingo.odata2.api.edm.EdmProperty;
+import org.apache.olingo.odata2.api.edm.EdmServiceMetadata;
+import org.apache.olingo.odata2.api.ep.EntityProvider;
+import org.apache.olingo.odata2.api.ep.EntityProviderWriteProperties;
+import org.apache.olingo.odata2.api.processor.ODataResponse;
+import org.eclipse.jetty.http.HttpHeader;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import okhttp3.HttpUrl;
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import okio.Buffer;
+
+/**
+ * Tests support for concurrency properties which generate and require
+ * reading eTags before patch, update and delete operations.
+ *
+ * Since the embedded olingo2 odata service does not contain any
+ * concurrency properties, it is necessary to mock up a new server.
+ *
+ * Uses a cutdown version of the reference odata service and adds in
+ * extra concurrency properties.
+ *
+ * Service's dispatcher only tests the correct calls are made and whether
+ * the eTags are correctly added as headers to the requisite requests.
+ *
+ */
+public class Olingo2AppAPIETagEnabledTest extends AbstractOlingo2AppAPITestSupport {
+
+ private static MockWebServer server;
+ private static Olingo2App olingoApp;
+ private static Edm edm;
+ private static EdmEntitySet manufacturersSet;
+
+ @BeforeClass
+ public static void scaffold() throws Exception {
+ initEdm();
+ initServer();
+ }
+
+ @AfterClass
+ public static void unscaffold() throws Exception {
+ if (olingoApp != null) {
+ olingoApp.close();
+ }
+ if (server != null) {
+ server.shutdown();
+ }
+ }
+
+ private static void initEdm() throws Exception {
+ InputStream edmXml = Olingo2AppAPIETagEnabledTest.class.getResourceAsStream("etag-enabled-service.xml");
+ edm = EntityProvider.readMetadata(edmXml, true);
+ assertNotNull(edm);
+
+ EdmEntityContainer entityContainer = edm.getDefaultEntityContainer();
+ assertNotNull(entityContainer);
+ manufacturersSet = entityContainer.getEntitySet(MANUFACTURERS);
+ assertNotNull(manufacturersSet);
+
+ EdmEntityType entityType = manufacturersSet.getEntityType();
+ assertNotNull(entityType);
+
+ //
+ // Check we have enabled eTag properties
+ //
+ EdmProperty property = (EdmProperty) entityType.getProperty("Id");
+ assertNotNull(property.getFacets());
+ }
+
+ private static void initServer() throws Exception {
+ server = new MockWebServer();
+ //
+ // Init dispatcher prior to start of server
+ //
+ server.setDispatcher(new Dispatcher() {
+
+ @SuppressWarnings( "resource" )
+ @Override
+ public MockResponse dispatch(RecordedRequest recordedRequest) throws InterruptedException {
+ MockResponse mockResponse = new MockResponse();
+
+ switch(recordedRequest.getMethod()) {
+ case HttpMethod.GET:
+ try {
+ if (recordedRequest.getPath().endsWith("/" + TEST_CREATE_MANUFACTURER)) {
+
+ ODataResponse odataResponse = EntityProvider.writeEntry(TEST_FORMAT.getMimeType(),
+ manufacturersSet, getEntityData(),
+ EntityProviderWriteProperties
+ .serviceRoot(getServiceUrl().uri())
+ .build());
+ InputStream entityStream = odataResponse.getEntityAsStream();
+ mockResponse.setResponseCode(HttpStatusCodes.OK.getStatusCode());
+ mockResponse.setBody(new Buffer().readFrom(entityStream));
+ return mockResponse;
+
+ } else if (recordedRequest.getPath().endsWith("/" + Olingo2AppImpl.METADATA)) {
+
+ EdmServiceMetadata serviceMetadata = edm.getServiceMetadata();
+ return mockResponse
+ .setResponseCode(HttpStatusCodes.OK.getStatusCode())
+ .addHeader(ODataHttpHeaders.DATASERVICEVERSION, serviceMetadata.getDataServiceVersion())
+ .setBody(new Buffer().readFrom(serviceMetadata.getMetadata()));
+ }
+
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ break;
+ case HttpMethod.PATCH:
+ case HttpMethod.PUT:
+ case HttpMethod.POST:
+ case HttpMethod.DELETE:
+ //
+ // Objective of the test:
+ // The Read has to have been called by Olingo2AppImpl.argumentWithETag
+ // which should then populate the IF-MATCH header with the eTag value.
+ // Verify the eTag value is present.
+ //
+ assertNotNull(recordedRequest.getHeader(HttpHeader.IF_MATCH.asString()));
+
+ return mockResponse.setResponseCode(HttpStatusCodes.NO_CONTENT.getStatusCode());
+ }
+
+ mockResponse
+ .setResponseCode(HttpStatusCodes.NOT_FOUND.getStatusCode())
+ .setBody("{ status: \"Not Found\"}");
+ return mockResponse;
+ }
+ });
+ server.start();
+
+ //
+ // have to init olingoApp after start of server
+ // since getBaseUrl() will call server start
+ //
+ olingoApp = new Olingo2AppImpl(getServiceUrl() + "/");
+ olingoApp.setContentType(TEST_FORMAT_STRING);
+ }
+
+ private static HttpUrl getServiceUrl() {
+ if (server == null) {
+ fail("Test programming failure. Server not initialised");
+ }
+
+ return server.url(SERVICE_NAME);
+ }
+
+ @Test
+ public void testPatchEntityWithETag() throws Exception {
+ TestOlingo2ResponseHandler<HttpStatusCodes> statusHandler = new TestOlingo2ResponseHandler<>();
+
+ Map<String, Object> data = getEntityData();
+ @SuppressWarnings("unchecked")
+ Map<String, Object> address = (Map<String, Object>) data.get(ADDRESS);
+
+ data.put("Name", "MyCarManufacturer Renamed");
+ address.put("Street", "Main Street");
+
+ //
+ // Call patch
+ //
+ olingoApp.patch(edm, TEST_CREATE_MANUFACTURER, null, data, statusHandler);
+
+ HttpStatusCodes statusCode = statusHandler.await();
+ assertEquals(HttpStatusCodes.NO_CONTENT, statusCode);
+ }
+
+ @Test
+ public void testUpdateEntityWithETag() throws Exception {
+ TestOlingo2ResponseHandler<HttpStatusCodes> statusHandler = new TestOlingo2ResponseHandler<>();
+
+ Map<String, Object> data = getEntityData();
+ @SuppressWarnings("unchecked")
+ Map<String, Object> address = (Map<String, Object>) data.get(ADDRESS);
+
+ data.put("Name", "MyCarManufacturer Renamed");
+ address.put("Street", "Main Street");
+
+ //
+ // Call update
+ //
+ olingoApp.update(edm, TEST_CREATE_MANUFACTURER, null, data, statusHandler);
+
+ HttpStatusCodes statusCode = statusHandler.await();
+ assertEquals(HttpStatusCodes.NO_CONTENT, statusCode);
+ }
+
+ @Test
+ public void testDeleteEntityWithETag() throws Exception {
+ TestOlingo2ResponseHandler<HttpStatusCodes> statusHandler = new TestOlingo2ResponseHandler<>();
+
+ Map<String, Object> data = getEntityData();
+ @SuppressWarnings("unchecked")
+ Map<String, Object> address = (Map<String, Object>) data.get(ADDRESS);
+
+ data.put("Name", "MyCarManufacturer Renamed");
+ address.put("Street", "Main Street");
+
+ //
+ // Call delete
+ //
+ olingoApp.delete(TEST_CREATE_MANUFACTURER, null, statusHandler);
+
+ HttpStatusCodes statusCode = statusHandler.await();
+ assertEquals(HttpStatusCodes.NO_CONTENT, statusCode);
+ }
+}
diff --git a/components/camel-olingo2/camel-olingo2-component/src/test/java/org/apache/camel/component/olingo2/Olingo2AppAPITest.java b/components/camel-olingo2/camel-olingo2-component/src/test/java/org/apache/camel/component/olingo2/Olingo2AppAPITest.java
index edb204a..1c1f281 100644
--- a/components/camel-olingo2/camel-olingo2-component/src/test/java/org/apache/camel/component/olingo2/Olingo2AppAPITest.java
+++ b/components/camel-olingo2/camel-olingo2-component/src/test/java/org/apache/camel/component/olingo2/Olingo2AppAPITest.java
@@ -16,19 +16,14 @@
*/
package org.apache.camel.component.olingo2;
import java.io.InputStream;
-import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-
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;
@@ -36,8 +31,6 @@ import org.apache.camel.component.olingo2.api.batch.Olingo2BatchResponse;
import org.apache.camel.component.olingo2.api.batch.Operation;
import org.apache.camel.component.olingo2.api.impl.Olingo2AppImpl;
import org.apache.camel.component.olingo2.api.impl.SystemQueryOption;
-import org.apache.camel.test.AvailablePortFinder;
-import org.apache.http.entity.ContentType;
import org.apache.olingo.odata2.api.commons.HttpStatusCodes;
import org.apache.olingo.odata2.api.edm.Edm;
import org.apache.olingo.odata2.api.edm.EdmEntitySet;
@@ -45,63 +38,22 @@ import org.apache.olingo.odata2.api.edm.EdmEntitySetInfo;
import org.apache.olingo.odata2.api.ep.EntityProvider;
import org.apache.olingo.odata2.api.ep.EntityProviderReadProperties;
import org.apache.olingo.odata2.api.ep.entry.ODataEntry;
-import org.apache.olingo.odata2.api.ep.feed.ODataDeltaFeed;
import org.apache.olingo.odata2.api.ep.feed.ODataFeed;
import org.apache.olingo.odata2.api.servicedocument.Collection;
import org.apache.olingo.odata2.api.servicedocument.ServiceDocument;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Integration test for {@link org.apache.camel.component.olingo2.api.impl.Olingo2AppImpl}
* using the sample Olingo2 Server dynamically downloaded and started during the test.
*/
-public class Olingo2AppAPITest {
-
- private static final Logger LOG = LoggerFactory.getLogger(Olingo2AppAPITest.class);
- private static final int PORT = AvailablePortFinder.getNextAvailable();
-
- private static final long TIMEOUT = 10;
-
- private static final String MANUFACTURERS = "Manufacturers";
- private static final String FQN_MANUFACTURERS = "DefaultContainer.Manufacturers";
- private static final String ADDRESS = "Address";
- private static final String CARS = "Cars";
-
- private static final String TEST_KEY = "'1'";
- private static final String TEST_CREATE_KEY = "'123'";
- private static final String TEST_MANUFACTURER = FQN_MANUFACTURERS + "(" + TEST_KEY + ")";
- private static final String TEST_CREATE_MANUFACTURER = MANUFACTURERS + "(" + TEST_CREATE_KEY + ")";
-
- private static final String TEST_RESOURCE_CONTENT_ID = "1";
- private static final String TEST_RESOURCE = "$" + TEST_RESOURCE_CONTENT_ID;
-
- private static final char NEW_LINE = '\n';
- private static final String TEST_CAR = "Manufacturers('1')/Cars('1')";
- private static final String TEST_MANUFACTURER_FOUNDED_PROPERTY = "Manufacturers('1')/Founded";
- private static final String TEST_MANUFACTURER_FOUNDED_VALUE = "Manufacturers('1')/Founded/$value";
- private static final String FOUNDED_PROPERTY = "Founded";
- private static final String TEST_MANUFACTURER_ADDRESS_PROPERTY = "Manufacturers('1')/Address";
- private static final String TEST_MANUFACTURER_LINKS_CARS = "Manufacturers('1')/$links/Cars";
- private static final String TEST_CAR_LINK_MANUFACTURER = "Cars('1')/$links/Manufacturer";
- private static final String COUNT_OPTION = "/$count";
-
- private static final String TEST_SERVICE_URL = "http://localhost:" + PORT + "/MyFormula.svc";
- // private static final String TEST_SERVICE_URL = "http://localhost:8080/cars-annotations-sample/MyFormula.svc";
-// private static final ContentType TEST_FORMAT = ContentType.APPLICATION_XML_CS_UTF_8;
- private static final ContentType TEST_FORMAT = ContentType.APPLICATION_JSON;
- private static final String TEST_FORMAT_STRING = TEST_FORMAT.toString();
-// private static final Pattern LINK_PATTERN = Pattern.compile("[^(]+\\('([^']+)'\\)");
- private static final String ID_PROPERTY = "Id";
+public class Olingo2AppAPITest extends AbstractOlingo2AppAPITestSupport {
private static Olingo2App olingoApp;
private static Edm edm;
@@ -523,129 +475,4 @@ public class Olingo2AppAPITest {
assertNotNull(exception);
LOG.info("Batch retrieve deleted entry: {}", exception);
}
-
- private Map<String, Object> getEntityData() {
- Map<String, Object> data = new HashMap<>();
- data.put(ID_PROPERTY, "123");
- data.put("Name", "MyCarManufacturer");
- data.put(FOUNDED_PROPERTY, new Date());
- Map<String, Object> address = new HashMap<>();
- address.put("Street", "Main");
- address.put("ZipCode", "42421");
- address.put("City", "Fairy City");
- address.put("Country", "FarFarAway");
- data.put(ADDRESS, address);
- return data;
- }
-
- private static String prettyPrint(ODataFeed dataFeed) {
- StringBuilder builder = new StringBuilder();
- builder.append("[\n");
- for (ODataEntry entry : dataFeed.getEntries()) {
- builder.append(prettyPrint(entry.getProperties(), 1)).append('\n');
- }
- builder.append("]\n");
- return builder.toString();
- }
-
- private static String prettyPrint(ODataEntry createdEntry) {
- return prettyPrint(createdEntry.getProperties(), 0);
- }
-
- private static String prettyPrint(Map<String, Object> properties, int level) {
- StringBuilder b = new StringBuilder();
- Set<Map.Entry<String, Object>> entries = properties.entrySet();
-
- for (Map.Entry<String, Object> entry : entries) {
- indent(b, level);
- b.append(entry.getKey()).append(": ");
- Object value = entry.getValue();
- if (value instanceof Map) {
- @SuppressWarnings("unchecked")
- final Map<String, Object> objectMap = (Map<String, Object>) value;
- value = prettyPrint(objectMap, level + 1);
- b.append(value).append(NEW_LINE);
- } else if (value instanceof Calendar) {
- Calendar cal = (Calendar) value;
- value = DateFormat.getInstance().format(cal.getTime());
- b.append(value).append(NEW_LINE);
- } else if (value instanceof ODataDeltaFeed) {
- ODataDeltaFeed feed = (ODataDeltaFeed) value;
- List<ODataEntry> inlineEntries = feed.getEntries();
- b.append("{");
- for (ODataEntry oDataEntry : inlineEntries) {
- value = prettyPrint(oDataEntry.getProperties(), level + 1);
- b.append("\n[\n").append(value).append("\n],");
- }
- b.deleteCharAt(b.length() - 1);
- indent(b, level);
- b.append("}\n");
- } else {
- b.append(value).append(NEW_LINE);
- }
- }
- // remove last line break
- b.deleteCharAt(b.length() - 1);
- return b.toString();
- }
-
- private static void indent(StringBuilder builder, int indentLevel) {
- for (int i = 0; i < indentLevel; i++) {
- builder.append(" ");
- }
- }
-
- private static final class TestOlingo2ResponseHandler<T> implements Olingo2ResponseHandler<T> {
-
- private T response;
- private Exception error;
- private CountDownLatch latch = new CountDownLatch(1);
-
- @Override
- public void onResponse(T response, Map<String, String> responseHeaders) {
- this.response = response;
- if (LOG.isDebugEnabled()) {
- if (response instanceof ODataFeed) {
- LOG.debug("Received response: {}", prettyPrint((ODataFeed) response));
- } else if (response instanceof ODataEntry) {
- LOG.debug("Received response: {}", prettyPrint((ODataEntry) response));
- } else {
- LOG.debug("Received response: {}", response);
- }
- }
- latch.countDown();
- }
-
- @Override
- public void onException(Exception ex) {
- error = ex;
- latch.countDown();
- }
-
- @Override
- public void onCanceled() {
- error = new IllegalStateException("Request Canceled");
- latch.countDown();
- }
-
- public T await() throws Exception {
- return await(TIMEOUT, TimeUnit.SECONDS);
- }
-
- public T await(long timeout, TimeUnit unit) throws Exception {
- assertTrue("Timeout waiting for response", latch.await(timeout, unit));
- if (error != null) {
- throw error;
- }
- assertNotNull("Response", response);
- return response;
- }
-
- public void reset() {
- latch.countDown();
- latch = new CountDownLatch(1);
- response = null;
- error = null;
- }
- }
}
diff --git a/components/camel-olingo2/camel-olingo2-component/src/test/resources/org/apache/camel/component/olingo2/etag-enabled-service.xml b/components/camel-olingo2/camel-olingo2-component/src/test/resources/org/apache/camel/component/olingo2/etag-enabled-service.xml
new file mode 100644
index 0000000..65e6369
--- /dev/null
+++ b/components/camel-olingo2/camel-olingo2-component/src/test/resources/org/apache/camel/component/olingo2/etag-enabled-service.xml
@@ -0,0 +1,26 @@
+<edmx:Edmx xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx" Version="1.0">
+ <script/>
+ <edmx:DataServices xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" m:DataServiceVersion="1.0">
+ <Schema xmlns="http://schemas.microsoft.com/ado/2008/09/edm" Namespace="MyFormula">
+ <EntityType Name="Manufacturer">
+ <Key>
+ <PropertyRef Name="Id"/>
+ </Key>
+ <!-- Includes concurrency support which is then handled using ETags -->
+ <Property Name="Id" Type="Edm.String" Nullable="true" ConcurrencyMode="Fixed"/>
+ <Property Name="Name" Type="Edm.String" Nullable="true" ConcurrencyMode="Fixed"/>
+ <Property Name="Founded" Type="Edm.DateTimeOffset" Nullable="true" ConcurrencyMode="Fixed"/>
+ <Property Name="Address" Type="MyFormula.Address" Nullable="true" ConcurrencyMode="Fixed"/>
+ </EntityType>
+ <ComplexType Name="Address">
+ <Property Name="Street" Type="Edm.String" Nullable="true"/>
+ <Property Name="City" Type="Edm.String" Nullable="true"/>
+ <Property Name="ZipCode" Type="Edm.String" Nullable="true"/>
+ <Property Name="Country" Type="Edm.String" Nullable="true"/>
+ </ComplexType>
+ <EntityContainer Name="DefaultContainer" m:IsDefaultEntityContainer="true">
+ <EntitySet Name="Manufacturers" EntityType="MyFormula.Manufacturer"/>
+ </EntityContainer>
+ </Schema>
+ </edmx:DataServices>
+</edmx:Edmx>
\ No newline at end of file
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
index bf0b2ed..87f4ccb 100644
--- 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
@@ -28,7 +28,9 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.UUID;
+import java.util.function.Consumer;
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;
@@ -56,6 +58,7 @@ 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.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.config.MessageConstraints;
@@ -276,32 +279,50 @@ public final class Olingo4AppImpl implements Olingo4App {
public <T> void update(final Edm edm, final String resourcePath, final Map<String, String> endpointHttpHeaders, final Object data, final Olingo4ResponseHandler<T> responseHandler) {
final UriInfo uriInfo = parseUri(edm, resourcePath, null, serviceUri);
- writeContent(edm, new HttpPut(createUri(resourcePath, null)), uriInfo, data, endpointHttpHeaders, responseHandler);
+ augmentWithETag(edm, resourcePath, endpointHttpHeaders,
+ new HttpPut(createUri(resourcePath, null)),
+ request -> writeContent(edm, (HttpPut) request, uriInfo, data, endpointHttpHeaders, responseHandler),
+ responseHandler);
}
@Override
public void delete(final String resourcePath, final Map<String, String> endpointHttpHeaders, final Olingo4ResponseHandler<HttpStatusCode> responseHandler) {
- execute(new HttpDelete(createUri(resourcePath)), contentType, endpointHttpHeaders, new AbstractFutureCallback<HttpStatusCode>(responseHandler) {
- @Override
- public void onCompleted(HttpResponse result) {
- final StatusLine statusLine = result.getStatusLine();
- responseHandler.onResponse(HttpStatusCode.fromStatusCode(statusLine.getStatusCode()), headersToMap(result.getAllHeaders()));
- }
- });
+ HttpDelete deleteRequest = new HttpDelete(createUri(resourcePath));
+
+ Consumer<HttpRequestBase> deleteFunction = (request) -> {
+ execute(request, contentType, endpointHttpHeaders, new AbstractFutureCallback<HttpStatusCode>(responseHandler) {
+ @Override
+ public void onCompleted(HttpResponse result) {
+ final StatusLine statusLine = result.getStatusLine();
+ responseHandler.onResponse(HttpStatusCode.fromStatusCode(statusLine.getStatusCode()), headersToMap(result.getAllHeaders()));
+ }
+ });
+ };
+
+ augmentWithETag(null, resourcePath, endpointHttpHeaders,
+ deleteRequest,
+ deleteFunction,
+ responseHandler);
}
@Override
public <T> void patch(final Edm edm, final String resourcePath, final Map<String, String> endpointHttpHeaders, final Object data, final Olingo4ResponseHandler<T> responseHandler) {
final UriInfo uriInfo = parseUri(edm, resourcePath, null, serviceUri);
- writeContent(edm, new HttpPatch(createUri(resourcePath, null)), uriInfo, data, endpointHttpHeaders, responseHandler);
+ augmentWithETag(edm, resourcePath, endpointHttpHeaders,
+ new HttpPatch(createUri(resourcePath, null)),
+ request -> writeContent(edm, (HttpPatch) request, uriInfo, data, endpointHttpHeaders, responseHandler),
+ responseHandler);
}
@Override
public <T> void merge(final Edm edm, final String resourcePath, final Map<String, String> endpointHttpHeaders, final Object data, final Olingo4ResponseHandler<T> responseHandler) {
final UriInfo uriInfo = parseUri(edm, resourcePath, null, serviceUri);
- writeContent(edm, new HttpMerge(createUri(resourcePath, null)), uriInfo, data, endpointHttpHeaders, responseHandler);
+ augmentWithETag(edm, resourcePath, endpointHttpHeaders,
+ new HttpMerge(createUri(resourcePath, null)),
+ request -> writeContent(edm, (HttpMerge) request, uriInfo, data, endpointHttpHeaders, responseHandler),
+ responseHandler);
}
@Override
@@ -345,6 +366,91 @@ public final class Olingo4AppImpl implements Olingo4App {
return resourceContentType;
}
+ /**
+ * On occasion, some resources are protected with Optimistic Concurrency via the use of eTags.
+ * This will first conduct a read on the given entity resource, find its eTag then perform the given
+ * delegate request function, augmenting the request with the eTag, if appropriate.
+ *
+ * Since read operations may be asynchronous, it is necessary to chain together the methods via
+ * the use of a {@link Consumer} function. Only when the response from the read returns will
+ * this delegate function be executed.
+ *
+ * @param edm the Edm object to be interrogated
+ * @param resourcePath the resource path of the entity to be operated on
+ * @param endpointHttpHeaders the headers provided from the endpoint which may be required for the read operation
+ * @param httpRequest the request to be updated, if appropriate, with the eTag and provided to the delegate request function
+ * @param delegateRequestFn the function to be invoked in response to the read operation
+ * @param delegateResponseHandler the response handler to respond if any errors occur during the read operation
+ */
+ private <T> void augmentWithETag(final Edm edm, final String resourcePath, final Map<String, String> endpointHttpHeaders,
+ final HttpRequestBase httpRequest,
+ final Consumer<HttpRequestBase> delegateRequestFn,
+ final Olingo4ResponseHandler<T> delegateResponseHandler) {
+
+ if (edm == null) {
+ // Can be the case if calling a delete then need to do a metadata call first
+ final Olingo4ResponseHandler<Edm> edmResponseHandler = new Olingo4ResponseHandler<Edm>() {
+ @Override
+ public void onResponse(Edm response, Map<String, String> responseHeaders) {
+ //
+ // Call this method again with an intact edm object
+ //
+ augmentWithETag(response, resourcePath, endpointHttpHeaders, httpRequest, delegateRequestFn, delegateResponseHandler);
+ }
+
+ @Override
+ public void onException(Exception ex) {
+ delegateResponseHandler.onException(ex);
+ }
+
+ @Override
+ public void onCanceled() {
+ delegateResponseHandler.onCanceled();
+ }
+ };
+
+ //
+ // Reads the metadata to establish an Edm object
+ // then the response handler invokes this method again with the new edm object
+ //
+ read(null, Constants.METADATA, null, null, edmResponseHandler);
+
+ } else {
+
+ //
+ // The handler that responds to the read operation and supplies an ETag if necessary
+ // and invokes the delegate request function
+ //
+ Olingo4ResponseHandler<T> eTagReadHandler = new Olingo4ResponseHandler<T>() {
+
+ @Override
+ public void onResponse(T response, Map<String, String> responseHeaders) {
+ if (response instanceof ClientEntity) {
+ ClientEntity e = (ClientEntity) response;
+ Optional
+ .ofNullable(e.getETag())
+ .ifPresent(v -> httpRequest.addHeader("If-Match", v));
+ }
+
+ // Invoke the delegate request function providing the modified request
+ delegateRequestFn.accept(httpRequest);
+ }
+
+ @Override
+ public void onException(Exception ex) {
+ delegateResponseHandler.onException(ex);
+ }
+
+ @Override
+ public void onCanceled() {
+ delegateResponseHandler.onCanceled();
+ }
+ };
+
+ read(edm, resourcePath, null, endpointHttpHeaders, eTagReadHandler);
+ }
+ }
+
private <T> void readContent(UriInfo uriInfo, InputStream content, Map<String, String> endpointHttpHeaders, Olingo4ResponseHandler<T> responseHandler) {
try {
responseHandler.onResponse(this.<T> readContent(uriInfo, content), endpointHttpHeaders);
diff --git a/components/camel-olingo4/camel-olingo4-api/src/test/java/org/apache/camel/component/olingo4/Olingo4AppAPITest.java b/components/camel-olingo4/camel-olingo4-api/src/test/java/org/apache/camel/component/olingo4/Olingo4AppAPITest.java
index 7422298..d6c7d2e 100644
--- a/components/camel-olingo4/camel-olingo4-api/src/test/java/org/apache/camel/component/olingo4/Olingo4AppAPITest.java
+++ b/components/camel-olingo4/camel-olingo4-api/src/test/java/org/apache/camel/component/olingo4/Olingo4AppAPITest.java
@@ -30,7 +30,6 @@ import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-
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;
@@ -95,6 +94,8 @@ public class Olingo4AppAPITest {
private static final String PEOPLE = "People";
private static final String TEST_PEOPLE = "People('russellwhyte')";
private static final String TEST_AIRLINE = "Airlines('FM')";
+ private static final String TEST_AIRLINE_TO_UPDATE = "Airlines('AA')"; // Careful using this as it get updated!
+ private static final String TEST_AIRLINE_TO_DELETE = "Airlines('MU')"; // Careful using this as it gets deleted!
private static final String TRIPS = "Trips";
private static final String TEST_CREATE_RESOURCE_CONTENT_ID = "1";
private static final String TEST_UPDATE_RESOURCE_CONTENT_ID = "2";
@@ -302,6 +303,140 @@ public class Olingo4AppAPITest {
LOG.info("People count: {}", count);
}
+ /**
+ * The Airline resource is implemented with Optimistic Concurrency.
+ * This requires an eTag to be first fetched via a read before performing
+ * patch, update, delete or merge operations.
+ *
+ * The test should complete successfully and not throw an error of the form
+ * 'The request need to have If-Match or If-None-Match header'
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testDeleteOptimisticConcurrency() throws Exception {
+ // test simple property Airlines
+ final TestOlingo4ResponseHandler<ClientEntity> entityHandler = new TestOlingo4ResponseHandler<>();
+
+ olingoApp.read(edm, TEST_AIRLINE_TO_DELETE, null, null, entityHandler);
+
+ // Confirm presence of eTag
+ ClientEntity airline = entityHandler.await();
+ assertNotNull(airline);
+ assertNotNull(airline.getETag());
+
+ TestOlingo4ResponseHandler<HttpStatusCode> statusHandler = new TestOlingo4ResponseHandler<>();
+
+ //
+ // Call delete
+ //
+ olingoApp.delete(TEST_AIRLINE_TO_DELETE, null, statusHandler);
+
+ HttpStatusCode statusCode = statusHandler.await();
+ assertEquals(HttpStatusCode.NO_CONTENT, statusCode);
+ LOG.info("Deleted entity at {}", TEST_AIRLINE_TO_DELETE);
+
+ // Check for deleted entity
+ final TestOlingo4ResponseHandler<HttpStatusCode> responseHandler = new TestOlingo4ResponseHandler<>();
+ olingoApp.read(edm, TEST_AIRLINE_TO_DELETE, null, null, responseHandler);
+
+ statusCode = statusHandler.await();
+ assertEquals(HttpStatusCode.NO_CONTENT, statusCode);
+ LOG.info("Deleted entity at {}", TEST_AIRLINE_TO_DELETE);
+ }
+
+ /**
+ * The Airline resource is implemented with Optimistic Concurrency.
+ * This requires an eTag to be first fetched via a read before performing
+ * patch, update, delete or merge operations.
+ *
+ * The test should complete successfully and not throw an error of the form
+ * 'The request need to have If-Match or If-None-Match header'
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testPatchOptimisticConcurrency() throws Exception {
+ // test simple property Airlines
+ final TestOlingo4ResponseHandler<ClientEntity> entityHandler = new TestOlingo4ResponseHandler<>();
+
+ olingoApp.read(edm, TEST_AIRLINE_TO_UPDATE, null, null, entityHandler);
+
+ // Confirm presence of eTag
+ ClientEntity airline = entityHandler.await();
+ assertNotNull(airline);
+ assertNotNull(airline.getETag());
+
+ TestOlingo4ResponseHandler<HttpStatusCode> statusHandler = new TestOlingo4ResponseHandler<>();
+ ClientEntity clientEntity = objFactory.newEntity(null);
+ String newAirlineName = "The Patched American Airlines";
+ clientEntity.getProperties().add(objFactory.newPrimitiveProperty("Name",
+ objFactory.newPrimitiveValueBuilder().buildString(newAirlineName)));
+
+ //
+ // Call patch
+ //
+ olingoApp.patch(edm, TEST_AIRLINE_TO_UPDATE, null, clientEntity, statusHandler);
+
+ HttpStatusCode statusCode = statusHandler.await();
+ assertEquals(HttpStatusCode.NO_CONTENT, statusCode);
+ LOG.info("Name property updated with status {}", statusCode.getStatusCode());
+
+ // Check for updated entity
+ final TestOlingo4ResponseHandler<ClientEntity> responseHandler = new TestOlingo4ResponseHandler<>();
+
+ olingoApp.read(edm, TEST_AIRLINE_TO_UPDATE, null, null, responseHandler);
+ ClientEntity entity = responseHandler.await();
+ assertEquals(newAirlineName, entity.getProperty("Name").getValue().toString());
+ LOG.info("Updated Single Entity: {}", prettyPrint(entity));
+ }
+
+ /**
+ * The Airline resource is implemented with Optimistic Concurrency.
+ * This requires an eTag to be first fetched via a read before performing
+ * patch, update, delete or merge operations.
+ *
+ * The test should complete successfully and not throw an error of the form
+ * 'The request need to have If-Match or If-None-Match header'
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testUpdateOptimisticConcurrency() throws Exception {
+ // test simple property Airlines
+ final TestOlingo4ResponseHandler<ClientEntity> entityHandler = new TestOlingo4ResponseHandler<>();
+
+ olingoApp.read(edm, TEST_AIRLINE_TO_UPDATE, null, null, entityHandler);
+
+ // Confirm presence of eTag
+ ClientEntity airline = entityHandler.await();
+ assertNotNull(airline);
+ assertNotNull(airline.getETag());
+
+ TestOlingo4ResponseHandler<HttpStatusCode> statusHandler = new TestOlingo4ResponseHandler<>();
+ ClientEntity clientEntity = objFactory.newEntity(null);
+ String newAirlineName = "The Updated American Airlines";
+ clientEntity.getProperties().add(objFactory.newPrimitiveProperty("Name",
+ objFactory.newPrimitiveValueBuilder().buildString(newAirlineName)));
+
+ //
+ // Call update
+ //
+ olingoApp.update(edm, TEST_AIRLINE_TO_UPDATE, null, clientEntity, statusHandler);
+
+ HttpStatusCode statusCode = statusHandler.await();
+ assertEquals(HttpStatusCode.NO_CONTENT, statusCode);
+ LOG.info("Name property updated with status {}", statusCode.getStatusCode());
+
+ // Check for updated entity
+ final TestOlingo4ResponseHandler<ClientEntity> responseHandler = new TestOlingo4ResponseHandler<>();
+
+ olingoApp.read(edm, TEST_AIRLINE_TO_UPDATE, null, null, responseHandler);
+ ClientEntity entity = responseHandler.await();
+ assertEquals(newAirlineName, entity.getProperty("Name").getValue().toString());
+ LOG.info("Updated Single Entity: {}", prettyPrint(entity));
+ }
+
@Test
public void testCreateUpdateDeleteEntity() throws Exception {
diff --git a/components/camel-olingo4/camel-olingo4-component/src/test/java/org/apache/camel/component/olingo4/Olingo4ComponentProducerTest.java b/components/camel-olingo4/camel-olingo4-component/src/test/java/org/apache/camel/component/olingo4/Olingo4ComponentProducerTest.java
index 46e899e..2537347 100644
--- a/components/camel-olingo4/camel-olingo4-component/src/test/java/org/apache/camel/component/olingo4/Olingo4ComponentProducerTest.java
+++ b/components/camel-olingo4/camel-olingo4-component/src/test/java/org/apache/camel/component/olingo4/Olingo4ComponentProducerTest.java
@@ -156,7 +156,8 @@ public class Olingo4ComponentProducerTest extends AbstractOlingo4TestSupport {
try {
requestBody("direct:read-deleted-entity", null);
} catch (CamelExecutionException e) {
- assertEquals("Resource Not Found [HTTP/1.1 404 Not Found]", e.getCause().getMessage());
+ String causeMsg = e.getCause().getMessage();
+ assertTrue(causeMsg.contains("[HTTP/1.1 404 Not Found]"));
}
}
@@ -185,7 +186,8 @@ public class Olingo4ComponentProducerTest extends AbstractOlingo4TestSupport {
try {
requestBody("direct:read-deleted-entity", null);
} catch (CamelExecutionException e) {
- assertEquals("Resource Not Found [HTTP/1.1 404 Not Found]", e.getCause().getMessage());
+ String causeMsg = e.getCause().getMessage();
+ assertTrue(causeMsg.contains("[HTTP/1.1 404 Not Found]"));
}
}