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]"));
         }
     }