You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by zr...@apache.org on 2017/11/08 10:58:03 UTC

[camel] branch master updated: CAMEL-11995: Salesforce Composite API support

This is an automated email from the ASF dual-hosted git repository.

zregvart pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/master by this push:
     new 5821936  CAMEL-11995: Salesforce Composite API support
5821936 is described below

commit 58219362d1de4a37e150d19995c4c656fc5037bd
Author: Zoran Regvart <zr...@apache.org>
AuthorDate: Tue Nov 7 13:52:35 2017 +0100

    CAMEL-11995: Salesforce Composite API support
    
    Adds support for third Composite resource, now we can support Batch,
    Tree and plain "Composite" resource. Composite resource allows
    generating a request of up to 25 possibly chained requests. Chaining is
    performed by using references so response from a previous request can be
    used in the subsequent request.
    
    Bulk of the work was contributed by Spiliopoulos, Vassilis (ELS-CON)
    <v....@elsevier.com>, this polishes and rebases on the current
    master.
---
 .../src/main/docs/salesforce-component.adoc        |  53 ++-
 .../component/salesforce/SalesforceEndpoint.java   |   2 +-
 .../component/salesforce/SalesforceProducer.java   |   7 +-
 .../api/dto/composite/CompositeRequest.java        |  82 ++++
 .../api/dto/composite/SObjectComposite.java        | 414 +++++++++++++++++++++
 .../dto/composite/SObjectCompositeResponse.java    |  50 +++
 .../api/dto/composite/SObjectCompositeResult.java  |  76 ++++
 .../api/dto/composite/SObjectTreeResponse.java     |   5 +-
 .../salesforce/internal/OperationName.java         |   3 +-
 .../internal/client/CompositeApiClient.java        |   5 +
 .../internal/client/DefaultCompositeApiClient.java | 174 +++++----
 .../processor/AbstractSalesforceProcessor.java     |  35 +-
 .../internal/processor/CompositeApiProcessor.java  |  46 ++-
 .../CompositeApiBatchIntegrationTest.java          |   5 +
 .../salesforce/CompositeApiIntegrationTest.java    | 256 +++++++++++++
 .../composite/SObjectCompositeResponseTest.java    | 139 +++++++
 .../api/dto/composite/SObjectCompositeTest.java    | 119 ++++++
 .../api/dto/composite_request_example.json         |  31 ++
 .../dto/composite_response_example_failure.json    |  20 +
 .../dto/composite_response_example_success.json    |  27 ++
 20 files changed, 1447 insertions(+), 102 deletions(-)

diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/docs/salesforce-component.adoc b/components/camel-salesforce/camel-salesforce-component/src/main/docs/salesforce-component.adoc
index b50f940..11ac478 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/docs/salesforce-component.adoc
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/docs/salesforce-component.adoc
@@ -136,6 +136,7 @@ results) using result link returned from the 'query' API
 * recent - fetching recent items
 * approval - submit a record or records (batch) for approval process
 * approvals - fetch a list of all approval processes
+* composite - submit up to 25 possibly related REST requests and receive individual responses
 * composite-tree - create up to 200 records with parent-child relationships (up to 5 levels) in one go
 * composite-batch - submit a composition of requests in batch
 
@@ -430,7 +431,7 @@ final String firstId = succeeded.get(0).getId();
 ### Using Salesforce Composite API to submit multiple requests in a batch
 The Composite API batch operation (`composite-batch`) allows you to accumulate multiple requests in a batch and then
 submit them in one go, saving the round trip cost of multiple individual requests. Each response is then received in a
-list of responses with the order perserved, so that the n-th requests response is in the n-th place of the response.
+list of responses with the order preserved, so that the n-th requests response is in the n-th place of the response.
 
 NOTE: The results can vary from API to API so the result of the request is given as a `java.lang.Object`. In most cases
 the result will be a `java.util.Map` with string keys and values or other `java.util.Map` as value. Requests made in
@@ -484,6 +485,56 @@ final Object updateResultData = deleteResult.getResult(); // probably null
 
 -----------------------------------------------------------------------------------------------------
 
+### Using Salesforce Composite API to submit multiple chained requests
+The `composite` operation allows submitting up to 25 requests that can be chained together, for instance identifier
+generated in previous request can be used in subsequent request. Individual requests and responses are linked with the
+provided _reference_.
+
+NOTE: Composite API supports only JSON payloads.
+
+NOTE: As with the batch API the results can vary from API to API so the result of the request is given as a
+`java.lang.Object`. In most cases the result will be a `java.util.Map` with string keys and values or other
+`java.util.Map` as value. Requests made in JSON format hold some type information (i.e. it is known what values are
+strings and what values are numbers), so in general those will be more type friendly.
+
+Lets look at an example:
+
+[source,java]
+-----------------------------------------------------------------------------------------------------
+SObjectComposite composite = new SObjectComposite("38.0", true);
+
+// first insert operation via an external id
+final Account updateAccount = new TestAccount();
+updateAccount.setName("Salesforce");
+updateAccount.setBillingStreet("Landmark @ 1 Market Street");
+updateAccount.setBillingCity("San Francisco");
+updateAccount.setBillingState("California");
+updateAccount.setIndustry(Account_IndustryEnum.TECHNOLOGY);
+composite.addUpdate("Account", "001xx000003DIpcAAG", updateAccount, "UpdatedAccount");
+
+final Contact newContact = new TestContact();
+newContact.setLastName("John Doe");
+newContact.setPhone("1234567890");
+composite.addCreate(newContact, "NewContact");
+
+final AccountContactJunction__c junction = new AccountContactJunction__c();
+junction.setAccount__c("001xx000003DIpcAAG");
+junction.setContactId__c("@{NewContact.id}");
+composite.addCreate(junction, "JunctionRecord");
+
+final SObjectCompositeResponse response = template.requestBody("salesforce:composite?format=JSON", composite, SObjectCompositeResponse.class);
+final List<SObjectCompositeResult> results = response.getCompositeResponse();
+
+final SObjectCompositeResult accountUpdateResult = results.stream().filter(r -> "UpdatedAccount".equals(r.getReferenceId())).findFirst().get()
+final int statusCode = accountUpdateResult.getHttpStatusCode(); // should be 200
+final Map<String, ?> accountUpdateBody = accountUpdateResult.getBody();
+
+final SObjectCompositeResult contactCreationResult = results.stream().filter(r -> "JunctionRecord".equals(r.getReferenceId())).findFirst().get()
+// ...
+
+-----------------------------------------------------------------------------------------------------
+
+
 ### Camel Salesforce Maven Plugin
 
 This Maven plugin generates DTOs for the Camel
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpoint.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpoint.java
index f75e2b4..e41cf63 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpoint.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpoint.java
@@ -44,7 +44,7 @@ public class SalesforceEndpoint extends DefaultEndpoint {
         + "recent,createJob,getJob,closeJob,abortJob,createBatch,getBatch,getAllBatches,getRequest,getResults,"
         + "createBatchQuery,getQueryResultIds,getQueryResult,getRecentReports,getReportDescription,executeSyncReport,"
         + "executeAsyncReport,getReportInstances,getReportResults,limits,approval,approvals,composite-tree,"
-        + "composite-batch")
+        + "composite-batch,composite")
     private final OperationName operationName;
     @UriPath(label = "consumer", description = "The name of the topic to use")
     private final String topicName;
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceProducer.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceProducer.java
index 5c9c7e3..e332386 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceProducer.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceProducer.java
@@ -62,7 +62,7 @@ public class SalesforceProducer extends DefaultAsyncProducer {
         }
     }
 
-    private boolean isBulkOperation(OperationName operationName) {
+    private static boolean isBulkOperation(OperationName operationName) {
         switch (operationName) {
         case CREATE_JOB:
         case GET_JOB:
@@ -82,7 +82,7 @@ public class SalesforceProducer extends DefaultAsyncProducer {
         }
     }
 
-    private boolean isAnalyticsOperation(OperationName operationName) {
+    private static boolean isAnalyticsOperation(OperationName operationName) {
         switch (operationName) {
         case GET_RECENT_REPORTS:
         case GET_REPORT_DESCRIPTION:
@@ -96,10 +96,11 @@ public class SalesforceProducer extends DefaultAsyncProducer {
         }
     }
 
-    private boolean isCompositeOperation(OperationName operationName) {
+    private static  boolean isCompositeOperation(OperationName operationName) {
         switch (operationName) {
         case COMPOSITE_TREE:
         case COMPOSITE_BATCH:
+        case COMPOSITE:
             return true;
         default:
             return false;
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/CompositeRequest.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/CompositeRequest.java
new file mode 100644
index 0000000..72ffe7a
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/CompositeRequest.java
@@ -0,0 +1,82 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.salesforce.api.dto.composite;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamConverter;
+
+import org.apache.camel.component.salesforce.api.dto.XStreamFieldOrder;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectComposite.Method;
+
+@XStreamAlias("compositeRequest")
+@XStreamFieldOrder({"method", "url", "referenceId", "body"})
+@JsonInclude(Include.NON_NULL)
+@JsonPropertyOrder({"method", "url", "referenceId", "body"})
+final class CompositeRequest implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @XStreamConverter(RichInputConverter.class)
+    private final Object body;
+
+    private final Method method;
+
+    private final String referenceId;
+
+    private final String url;
+
+    CompositeRequest(final Method method, final String url, final Object body, final String referenceId) {
+        this.method = method;
+        this.url = url;
+        this.body = body;
+        this.referenceId = referenceId;
+    }
+
+    CompositeRequest(final Method method, final String url, final String referenceId) {
+        this.method = method;
+        this.url = url;
+        this.referenceId = referenceId;
+        body = null;
+    }
+
+    public Object getBody() {
+        return body;
+    }
+
+    public Method getMethod() {
+        return method;
+    }
+
+    public String getReferenceId() {
+        return referenceId;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    @Override
+    public String toString() {
+        return "Batch: " + method + " " + url + ", " + referenceId + ", data:" + body;
+    }
+
+}
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectComposite.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectComposite.java
new file mode 100644
index 0000000..864b2c4
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectComposite.java
@@ -0,0 +1,414 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.salesforce.api.dto.composite;
+
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamOmitField;
+
+import org.apache.camel.component.salesforce.api.dto.AbstractDescribedSObjectBase;
+import org.apache.camel.component.salesforce.api.dto.AbstractSObjectBase;
+import org.apache.camel.component.salesforce.api.utils.Version;
+import org.apache.camel.component.salesforce.internal.PayloadFormat;
+
+import static org.apache.camel.util.ObjectHelper.notNull;
+import static org.apache.camel.util.StringHelper.notEmpty;
+
+/**
+ * Executes a series of REST API requests in a single call. You can use the
+ * output of one request as the input to a subsequent request. The response
+ * bodies and HTTP statuses of the requests are returned in a single response
+ * body. The entire request counts as a single call toward your API limits. The
+ * requests in a composite call are called subrequests. All subrequests are
+ * executed in the context of the same user. In a subrequest’s body, you specify
+ * a reference ID that maps to the subrequest’s response. You can then refer to
+ * the ID in the url or body fields of later subrequests by using a
+ * JavaScript-like reference notation.
+ *
+ * Most requests that are supported in the Composite batch API the helper
+ * builder methods are provided. For batch requests that do not have their
+ * corresponding helper builder method, use {@link #addGeneric(Method, String)}
+ * or {@link #addGeneric(Method, String, Object)} methods. To build the batch
+ * use: <blockquote>
+ *
+ * <pre>
+ * {@code
+ * composite = new SObjectComposite("41.0", true);
+ *
+ * // insert operation via an external id
+ * final Invoice__c_Lookup invoiceLookup = new Invoice__c_Lookup();
+ * invoiceLookup.setInvoice_External_Id__c("0116");
+ *
+ * final Payment__c payment = new Payment__c();
+ * payment.setInvoice__r(invoiceLookup);
+ *
+ * composite.addCreate(payment, "NewPayment1");
+ * composite.addCreate(payment, "NewPayment2");
+ * }
+ *
+ * </pre>
+ *
+ * </blockquote>
+ *
+ * This will build a composite of two insert operations.
+ */
+@XStreamAlias("batch")
+public final class SObjectComposite implements Serializable {
+
+    public enum Method {
+        DELETE, GET, PATCH, POST
+    }
+
+    public static final PayloadFormat REQUIRED_PAYLOAD_FORMAT = PayloadFormat.JSON;
+
+    private static final int MAX_COMPOSITE_OPERATIONS = 25;
+
+    private static final long serialVersionUID = 1L;
+
+    private static final String SOBJECT_TYPE_PARAM = "type";
+
+    private final boolean allOrNone;
+
+    @XStreamOmitField
+    private final String apiPrefix;
+
+    private final List<CompositeRequest> compositeRequests = new ArrayList<>();
+
+    @XStreamOmitField
+    private final Version version;
+
+    /**
+     * Create new composite request. You must specify the API version of the
+     * batch request. The API version cannot be newer than the version
+     * configured in the Salesforce Camel component. Some of the batched
+     * requests are available only from certain Salesforce API versions, when
+     * this is the case it is noted in the documentation of the builder method,
+     * if uncertain consult the Salesforce API documentation.
+     *
+     * @param apiVersion API version for the batch request
+     */
+    public SObjectComposite(final String apiVersion, final boolean allOrNone) {
+        Objects.requireNonNull(apiVersion, "apiVersion");
+
+        version = Version.create(apiVersion);
+        this.allOrNone = allOrNone;
+        // composite API requires /services/data, in contrast to composite-batch
+        apiPrefix = "/services/data/v" + apiVersion;
+    }
+
+    /**
+     * Add create SObject to the composite request.
+     *
+     * @param data object to create
+     *
+     * @return this batch builder
+     */
+    public SObjectComposite addCreate(final AbstractDescribedSObjectBase data, final String referenceId) {
+        addCompositeRequest(
+            new CompositeRequest(Method.POST, apiPrefix + "/sobjects/" + typeOf(data) + "/", data, referenceId));
+
+        return this;
+    }
+
+    /**
+     * Add delete SObject with identifier to the composite request.
+     *
+     * @param type type of SObject
+     * @param id identifier of the object
+     * @return this batch builder
+     */
+    public SObjectComposite addDelete(final String type, final String id, final String referenceId) {
+        addCompositeRequest(new CompositeRequest(Method.DELETE, rowBaseUrl(type, id), referenceId));
+
+        return this;
+    }
+
+    /**
+     * Generic way to add requests to composite with {@code richInput} payload.
+     * Given URL starts from the version, so in order to update SObject specify
+     * just {@code /sobjects/Account/identifier} which results in
+     * {@code /services/data/v37.0/sobjects/Account/identifier}. Note the
+     * leading slash.
+     *
+     * @param method HTTP method
+     * @param url URL starting from the version
+     * @param richInput body of the request, to be placed in richInput
+     * @return this batch builder
+     */
+    public SObjectComposite addGeneric(final Method method, final String url, final Object richInput,
+        final String referenceId) {
+        addCompositeRequest(new CompositeRequest(method, apiPrefix + url, richInput, referenceId));
+
+        return this;
+    }
+
+    /**
+     * Generic way to add requests to composite. Given URL starts from the
+     * version, so in order to retrieve SObject specify just
+     * {@code /sobjects/Account/identifier} which results in
+     * {@code /services/data/v37.0/sobjects/Account/identifier}. Note the
+     * leading slash.
+     *
+     * @param method HTTP method
+     * @param url URL starting from the version
+     * @return this batch builder
+     */
+    public SObjectComposite addGeneric(final Method method, final String url, final String referenceId) {
+        addGeneric(method, url, null, referenceId);
+
+        return this;
+    }
+
+    /**
+     * Add field retrieval of an SObject by identifier to the composite request.
+     *
+     * @param type type of SObject
+     * @param id identifier of SObject
+     * @param fields to return
+     * @return this batch builder
+     */
+    public SObjectComposite addGet(final String type, final String id, final String referenceId,
+        final String... fields) {
+        final String fieldsParameter = composeFieldsParameter(fields);
+
+        addCompositeRequest(new CompositeRequest(Method.GET, rowBaseUrl(type, id) + fieldsParameter, referenceId));
+
+        return this;
+    }
+
+    /**
+     * Add field retrieval of an SObject by external identifier to the composite
+     * request.
+     *
+     * @param type type of SObject
+     * @param fieldName external identifier field name
+     * @param fieldValue external identifier field value
+     * @param fields to return
+     * @return this batch builder
+     */
+    public SObjectComposite addGetByExternalId(final String type, final String fieldName, final String fieldValue,
+        final String referenceId) {
+        addCompositeRequest(new CompositeRequest(Method.GET, rowBaseUrl(type, fieldName, fieldValue), referenceId));
+
+        return this;
+    }
+
+    /**
+     * Add retrieval of related SObject fields by identifier. For example
+     * {@code Account} has a relation to {@code CreatedBy}. To fetch fields from
+     * that related object ({@code User} SObject) use: <blockquote>
+     *
+     * <pre>
+     * {@code batch.addGetRelated("Account", identifier, "CreatedBy", "Name", "Id")}
+     * </pre>
+     *
+     * </blockquote>
+     *
+     * @param type type of SObject
+     * @param id identifier of SObject
+     * @param relation name of the related SObject field
+     * @param fields to return
+     * @return this batch builder
+     */
+    public SObjectComposite addGetRelated(final String type, final String id, final String relation,
+        final String referenceId, final String... fields) {
+        version.requireAtLeast(36, 0);
+
+        final String fieldsParameter = composeFieldsParameter(fields);
+
+        addCompositeRequest(new CompositeRequest(Method.GET,
+            rowBaseUrl(type, id) + "/" + notEmpty(relation, "relation") + fieldsParameter, referenceId));
+
+        return this;
+    }
+
+    /**
+     * Add retrieval of SObject records by query to the composite.
+     *
+     * @param query SOQL query to execute
+     * @return this batch builder
+     */
+    public SObjectComposite addQuery(final String query, final String referenceId) {
+        addCompositeRequest(
+            new CompositeRequest(Method.GET, apiPrefix + "/query/?q=" + notEmpty(query, "query"), referenceId));
+
+        return this;
+    }
+
+    /**
+     * Add retrieval of all SObject records by query to the composite.
+     *
+     * @param query SOQL query to execute
+     * @return this batch builder
+     */
+    public SObjectComposite addQueryAll(final String query, final String referenceId) {
+        addCompositeRequest(
+            new CompositeRequest(Method.GET, apiPrefix + "/queryAll/?q=" + notEmpty(query, "query"), referenceId));
+
+        return this;
+    }
+
+    /**
+     * Add update of SObject record to the composite. The given {@code data}
+     * parameter must contain only the fields that need updating and must not
+     * contain the {@code Id} field. So set any fields to {@code null} that you
+     * do not want changed along with {@code Id} field.
+     *
+     * @param type type of SObject
+     * @param id identifier of SObject
+     * @param data SObject with fields to change
+     * @return this batch builder
+     */
+    public SObjectComposite addUpdate(final String type, final String id, final AbstractSObjectBase data,
+        final String referenceId) {
+        addCompositeRequest(
+            new CompositeRequest(Method.PATCH, rowBaseUrl(type, notEmpty(id, "data.Id")), data, referenceId));
+
+        return this;
+    }
+
+    /**
+     * Add update of SObject record by external identifier to the composite. The
+     * given {@code data} parameter must contain only the fields that need
+     * updating and must not contain the {@code Id} field. So set any fields to
+     * {@code null} that you do not want changed along with {@code Id} field.
+     *
+     * @param type type of SObject
+     * @param fieldName name of the field holding the external identifier
+     * @param id external identifier value
+     * @param data SObject with fields to change
+     * @return this batch builder
+     */
+    public SObjectComposite addUpdateByExternalId(final String type, final String fieldName, final String fieldValue,
+        final AbstractSObjectBase data, final String referenceId) {
+
+        addCompositeRequest(
+            new CompositeRequest(Method.PATCH, rowBaseUrl(type, fieldName, fieldValue), data, referenceId));
+
+        return this;
+    }
+
+    /**
+     * Add insert or update of SObject record by external identifier to the
+     * composite. The given {@code data} parameter must contain only the fields
+     * that need updating and must not contain the {@code Id} field. So set any
+     * fields to {@code null} that you do not want changed along with {@code Id}
+     * field.
+     *
+     * @param type type of SObject
+     * @param fieldName name of the field holding the external identifier
+     * @param id external identifier value
+     * @param data SObject with fields to change
+     * @return this batch builder
+     */
+    public SObjectComposite addUpsertByExternalId(final String type, final String fieldName, final String fieldValue,
+        final AbstractSObjectBase data, final String referenceId) {
+
+        return addUpdateByExternalId(type, fieldName, fieldValue, data, referenceId);
+    }
+
+    public boolean getAllOrNone() {
+        return allOrNone;
+    }
+
+    /**
+     * Fetches compose requests contained in this compose request.
+     *
+     * @return all requests
+     */
+    @JsonProperty("compositeRequest")
+    public List<CompositeRequest> getCompositeRequests() {
+        return Collections.unmodifiableList(compositeRequests);
+    }
+
+    /**
+     * Version of Salesforce API for this batch request.
+     *
+     * @return the version
+     */
+    @JsonIgnore
+    public Version getVersion() {
+        return version;
+    }
+
+    /**
+     * Returns all object types nested within this composite request, needed for
+     * serialization.
+     *
+     * @return all object types in this composite request
+     */
+    @SuppressWarnings("rawtypes")
+    public Class[] objectTypes() {
+        final Set<Class<?>> types = Stream
+            .concat(Stream.of(SObjectComposite.class, BatchRequest.class), compositeRequests.stream()
+                .map(CompositeRequest::getBody).filter(Objects::nonNull).map(Object::getClass))
+            .collect(Collectors.toSet());
+
+        return types.toArray(new Class[types.size()]);
+    }
+
+    void addCompositeRequest(final CompositeRequest compositeRequest) {
+        if (compositeRequests.size() >= MAX_COMPOSITE_OPERATIONS) {
+            throw new IllegalArgumentException("You can add up to " + MAX_COMPOSITE_OPERATIONS
+                + " requests in a single composite request. Split your requests across multiple composite request.");
+        }
+        compositeRequests.add(compositeRequest);
+    }
+
+    String rowBaseUrl(final String type, final String id) {
+        return apiPrefix + "/sobjects/" + notEmpty(type, SOBJECT_TYPE_PARAM) + "/" + notEmpty(id, "id");
+    }
+
+    String rowBaseUrl(final String type, final String fieldName, final String fieldValue) {
+        try {
+            return apiPrefix + "/sobjects/" + notEmpty(type, SOBJECT_TYPE_PARAM) + "/"
+                + notEmpty(fieldName, "fieldName") + "/"
+                + URLEncoder.encode(notEmpty(fieldValue, "fieldValue"), StandardCharsets.UTF_8.name());
+        } catch (final UnsupportedEncodingException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    static String typeOf(final AbstractDescribedSObjectBase data) {
+        return notNull(data, "data").description().getName();
+    }
+
+    static String composeFieldsParameter(final String... fields) {
+        if (fields != null && fields.length > 0) {
+            try {
+                return "?fields=" + URLEncoder.encode(String.join(",", fields), StandardCharsets.UTF_8.name());
+            } catch (UnsupportedEncodingException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+
+        return "";
+    }
+}
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeResponse.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeResponse.java
new file mode 100644
index 0000000..a5abaf0
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeResponse.java
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.salesforce.api.dto.composite;
+
+import java.io.Serializable;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * The response of the composite request it contains individual results of each
+ * request submitted in a request at the same index.
+ */
+@XStreamAlias("compositeResults")
+public final class SObjectCompositeResponse implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private final List<SObjectCompositeResult> compositeResponse;
+
+    @JsonCreator
+    public SObjectCompositeResponse(@JsonProperty("results") final List<SObjectCompositeResult> compositeResponse) {
+        this.compositeResponse = compositeResponse;
+    }
+
+    public List<SObjectCompositeResult> getCompositeResponse() {
+        return compositeResponse;
+    }
+
+    @Override
+    public String toString() {
+        return "compositeResponse: " + compositeResponse;
+    }
+}
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeResult.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeResult.java
new file mode 100644
index 0000000..5ea2802
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeResult.java
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.salesforce.api.dto.composite;
+
+import java.io.Serializable;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamConverter;
+
+/**
+ * Contains the individual result of Composite API request.
+ */
+@XStreamAlias("batchResult")
+public final class SObjectCompositeResult implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @XStreamConverter(MapOfMapsConverter.class)
+    private final Object body;
+
+    private final Map<String, String> httpHeaders;
+
+    private final int httpStatusCode;
+
+    private final String referenceId;
+
+    @JsonCreator
+    public SObjectCompositeResult(@JsonProperty("body") final Object body,
+        @JsonProperty("headers") final Map<String, String> headers,
+        @JsonProperty("httpStatusCode") final int httpStatusCode,
+        @JsonProperty("referenceID") final String referenceId) {
+        this.body = body;
+        httpHeaders = headers;
+        this.httpStatusCode = httpStatusCode;
+        this.referenceId = referenceId;
+    }
+
+    public Object getBody() {
+        return body;
+    }
+
+    public Map<String, String> getHttpHeaders() {
+        return httpHeaders;
+    }
+
+    public int getHttpStatusCode() {
+        return httpStatusCode;
+    }
+
+    public String getReferenceId() {
+        return referenceId;
+    }
+
+    @Override
+    public String toString() {
+        return "SObjectCompositeResult [body=" + body + ", headers=" + httpHeaders + ", httpStatusCode="
+            + httpStatusCode + ", referenceId=" + referenceId + "]";
+    }
+}
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectTreeResponse.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectTreeResponse.java
index 736d917..4501d5c 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectTreeResponse.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectTreeResponse.java
@@ -32,8 +32,9 @@ import org.apache.camel.component.salesforce.api.dto.RestError;
 /**
  * Response from the SObject tree Composite API invocation.
  */
-@XStreamAlias("Result") // you might be wondering why `Result` and not `SObjectTreeResponse as in documentation, well,
-                        // the difference between documentation and practice is usually found in practice
+@XStreamAlias("Result") // you might be wondering why `Result` and not `SObjectTreeResponse` as in documentation, well,
+                        // the difference between documentation and practice is usually found in practice, this depends
+                        // on the version of the API that's used
 public final class SObjectTreeResponse implements Serializable {
 
     private static final long serialVersionUID = 1L;
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/OperationName.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/OperationName.java
index 3f18dbc..fb18971 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/OperationName.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/OperationName.java
@@ -70,7 +70,8 @@ public enum OperationName {
 
     // Composite API
     COMPOSITE_TREE("composite-tree"),
-    COMPOSITE_BATCH("composite-batch");
+    COMPOSITE_BATCH("composite-batch"),
+    COMPOSITE("composite");
 
     private final String value;
 
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/CompositeApiClient.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/CompositeApiClient.java
index 5bab28b..a399faa 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/CompositeApiClient.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/CompositeApiClient.java
@@ -23,6 +23,8 @@ import java.util.Optional;
 import org.apache.camel.component.salesforce.api.SalesforceException;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatch;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResponse;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectComposite;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectCompositeResponse;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectTree;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectTreeResponse;
 
@@ -40,6 +42,9 @@ public interface CompositeApiClient {
         void onResponse(Optional<T> body, Map<String, String> headers, SalesforceException exception);
     }
 
+    void submitComposite(SObjectComposite composite, Map<String, List<String>> headers,
+        ResponseCallback<SObjectCompositeResponse> callback) throws SalesforceException;
+
     void submitCompositeBatch(SObjectBatch batch, Map<String, List<String>> headers,
         ResponseCallback<SObjectBatchResponse> callback) throws SalesforceException;
 
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/DefaultCompositeApiClient.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/DefaultCompositeApiClient.java
index 64b170b..9efc74b 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/DefaultCompositeApiClient.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/DefaultCompositeApiClient.java
@@ -48,6 +48,8 @@ import org.apache.camel.component.salesforce.api.dto.AnnotationFieldKeySorter;
 import org.apache.camel.component.salesforce.api.dto.RestError;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatch;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResponse;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectComposite;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectCompositeResponse;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectTree;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectTreeResponse;
 import org.apache.camel.component.salesforce.api.utils.DateTimeConverter;
@@ -70,11 +72,11 @@ import org.slf4j.LoggerFactory;
 
 public class DefaultCompositeApiClient extends AbstractClientBase implements CompositeApiClient {
 
-    private static final Class[] ADDITIONAL_TYPES = new Class[] {SObjectTree.class, SObjectTreeResponse.class,
-        SObjectBatchResponse.class};
-
     private static final Logger LOG = LoggerFactory.getLogger(DefaultCompositeApiClient.class);
 
+    // Composite (non-tree, non-batch) does not support XML format
+    private static final XStream NO_XSTREAM = null;
+
     private final PayloadFormat format;
 
     private ObjectMapper mapper;
@@ -83,7 +85,9 @@ public class DefaultCompositeApiClient extends AbstractClientBase implements Com
 
     private final Map<Class<?>, ObjectWriter> writters = new HashMap<>();
 
-    private final XStream xStream;
+    private final XStream xStreamCompositeBatch;
+
+    private final XStream xStreamCompositeTree;
 
     public DefaultCompositeApiClient(final SalesforceEndpointConfig configuration, final PayloadFormat format,
         final String version, final SalesforceSession session, final SalesforceHttpClient httpClient)
@@ -97,30 +101,31 @@ public class DefaultCompositeApiClient extends AbstractClientBase implements Com
             mapper = JsonUtils.createObjectMapper();
         }
 
-        xStream = configureXStream();
+        xStreamCompositeBatch = configureXStream(SObjectBatch.class, SObjectBatchResponse.class);
+
+        xStreamCompositeTree = configureXStream(SObjectTree.class, SObjectTreeResponse.class);
+        // newer Salesforce API versions return `<SObjectTreeResponse>` element, older versions
+        // return `<Result>` element
+        xStreamCompositeTree.alias("SObjectTreeResponse", SObjectTreeResponse.class);
     }
 
-    static XStream configureXStream() {
-        final PureJavaReflectionProvider reflectionProvider = new PureJavaReflectionProvider(
-            new FieldDictionary(new AnnotationFieldKeySorter()));
+    @Override
+    public void submitComposite(final SObjectComposite composite, final Map<String, List<String>> headers,
+        final ResponseCallback<SObjectCompositeResponse> callback) throws SalesforceException {
+        // composite interface supports only json payload
+        checkCompositeFormat(format, SObjectComposite.REQUIRED_PAYLOAD_FORMAT);
 
-        final XppDriver hierarchicalStreamDriver = new XppDriver(new NoNameCoder()) {
-            @Override
-            public HierarchicalStreamWriter createWriter(final Writer out) {
-                return new CompactWriter(out, getNameCoder());
-            }
+        final String url = versionUrl() + "composite";
 
-        };
+        final Request post = createRequest(HttpMethod.POST, url, headers);
 
-        final XStream xStream = new XStream(reflectionProvider, hierarchicalStreamDriver);
-        xStream.aliasSystemAttribute(null, "class");
-        xStream.ignoreUnknownElements();
-        XStreamUtils.addDefaultPermissions(xStream);
-        xStream.registerConverter(new DateTimeConverter());
-        xStream.setMarshallingStrategy(new TreeMarshallingStrategy());
-        xStream.processAnnotations(ADDITIONAL_TYPES);
+        final ContentProvider content = serialize(NO_XSTREAM, composite, composite.objectTypes());
+        post.content(content);
 
-        return xStream;
+        doHttpRequest(post,
+            (response, responseHeaders, exception) -> callback.onResponse(
+                tryToReadResponse(NO_XSTREAM, SObjectCompositeResponse.class, response), responseHeaders,
+                exception));
     }
 
     @Override
@@ -132,11 +137,13 @@ public class DefaultCompositeApiClient extends AbstractClientBase implements Com
 
         final Request post = createRequest(HttpMethod.POST, url, headers);
 
-        final ContentProvider content = serialize(batch, batch.objectTypes());
+        final ContentProvider content = serialize(xStreamCompositeBatch, batch, batch.objectTypes());
         post.content(content);
 
-        doHttpRequest(post, (response, responseHeaders, exception) -> callback
-            .onResponse(tryToReadResponse(SObjectBatchResponse.class, response), responseHeaders, exception));
+        doHttpRequest(post,
+            (response, responseHeaders, exception) -> callback.onResponse(
+                tryToReadResponse(xStreamCompositeBatch, SObjectBatchResponse.class, response), responseHeaders,
+                exception));
     }
 
     @Override
@@ -146,22 +153,16 @@ public class DefaultCompositeApiClient extends AbstractClientBase implements Com
 
         final Request post = createRequest(HttpMethod.POST, url, headers);
 
-        final ContentProvider content = serialize(tree, tree.objectTypes());
+        final ContentProvider content = serialize(xStreamCompositeTree, tree, tree.objectTypes());
         post.content(content);
 
-        doHttpRequest(post, (response, responseHeaders, exception) -> callback
-            .onResponse(tryToReadResponse(SObjectTreeResponse.class, response), responseHeaders, exception));
+        doHttpRequest(post,
+            (response, responseHeaders, exception) -> callback.onResponse(
+                tryToReadResponse(xStreamCompositeTree, SObjectTreeResponse.class, response), responseHeaders,
+                exception));
     }
 
-    static void checkCompositeBatchVersion(final String configuredVersion, final Version batchVersion)
-        throws SalesforceException {
-        if (Version.create(configuredVersion).compareTo(batchVersion) < 0) {
-            throw new SalesforceException("Component is configured with Salesforce API version " + configuredVersion
-                + ", but the payload of the Composite API batch operation requires at least " + batchVersion, 0);
-        }
-    }
-
-    Request createRequest(final HttpMethod method, final String url, Map<String, List<String>> headers) {
+    Request createRequest(final HttpMethod method, final String url, final Map<String, List<String>> headers) {
         final Request request = getRequest(method, url, headers);
 
         // setup authorization
@@ -185,13 +186,6 @@ public class DefaultCompositeApiClient extends AbstractClientBase implements Com
         return jsonReaderFor(expectedType).readValue(responseStream);
     }
 
-    <T> T fromXml(final InputStream responseStream) {
-        @SuppressWarnings("unchecked")
-        final T read = (T) xStream.fromXML(responseStream);
-
-        return read;
-    }
-
     ObjectReader jsonReaderFor(final Class<?> type) {
         return Optional.ofNullable(readers.get(type)).orElseGet(() -> mapper.readerFor(type));
     }
@@ -202,17 +196,16 @@ public class DefaultCompositeApiClient extends AbstractClientBase implements Com
         return Optional.ofNullable(writters.get(type)).orElseGet(() -> mapper.writerFor(type));
     }
 
-    ContentProvider serialize(final Object body, final Class<?>... additionalTypes) throws SalesforceException {
-        final InputStream stream;
+    ContentProvider serialize(final XStream xstream, final Object body, final Class<?>... additionalTypes)
+        throws SalesforceException {
+        // input stream as entity content is needed for authentication retries
         if (format == PayloadFormat.JSON) {
-            stream = toJson(body);
-        } else {
-            // must be XML
-            stream = toXml(body, additionalTypes);
+            return new InputStreamContentProvider(toJson(body));
         }
 
-        // input stream as entity content is needed for authentication retries
-        return new InputStreamContentProvider(stream);
+        // must be XML
+        xstream.processAnnotations(additionalTypes);
+        return new InputStreamContentProvider(toXml(xstream, body));
     }
 
     String servicesDataUrl() {
@@ -230,26 +223,18 @@ public class DefaultCompositeApiClient extends AbstractClientBase implements Com
         return new ByteArrayInputStream(jsonBytes);
     }
 
-    InputStream toXml(final Object obj, final Class<?>... additionalTypes) {
-        xStream.processAnnotations(additionalTypes);
-
-        final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        xStream.toXML(obj, out);
-
-        return new ByteArrayInputStream(out.toByteArray());
-    }
-
-    <T> Optional<T> tryToReadResponse(final Class<T> expectedType, final InputStream responseStream) {
+    <T> Optional<T> tryToReadResponse(final XStream xstream, final Class<T> expectedType,
+        final InputStream responseStream) {
         if (responseStream == null) {
             return Optional.empty();
         }
         try {
             if (format == PayloadFormat.JSON) {
                 return Optional.of(fromJson(expectedType, responseStream));
-            } else {
-                // must be XML
-                return Optional.of(fromXml(responseStream));
             }
+
+            // must be XML
+            return Optional.of(fromXml(xstream, responseStream));
         } catch (XStreamException | IOException e) {
             LOG.warn("Unable to read response from the Composite API", e);
             return Optional.empty();
@@ -268,8 +253,8 @@ public class DefaultCompositeApiClient extends AbstractClientBase implements Com
     protected SalesforceException createRestException(final Response response, final InputStream responseContent) {
         final List<RestError> errors;
         try {
-            errors = readErrorsFrom(responseContent, format, mapper, xStream);
-        } catch (IOException e) {
+            errors = readErrorsFrom(responseContent, format, mapper, xStreamCompositeTree);
+        } catch (final IOException e) {
             return new SalesforceException("Unable to read error response", e);
         }
 
@@ -288,4 +273,59 @@ public class DefaultCompositeApiClient extends AbstractClientBase implements Com
         request.getHeaders().put("Authorization", "Bearer " + accessToken);
     }
 
+    static void checkCompositeBatchVersion(final String configuredVersion, final Version batchVersion)
+        throws SalesforceException {
+        if (Version.create(configuredVersion).compareTo(batchVersion) < 0) {
+            throw new SalesforceException("Component is configured with Salesforce API version " + configuredVersion
+                + ", but the payload of the Composite API batch operation requires at least " + batchVersion, 0);
+        }
+    }
+
+    static void checkCompositeFormat(final PayloadFormat configuredFormat, final PayloadFormat requiredFormat)
+        throws SalesforceException {
+        if (configuredFormat != requiredFormat) {
+            throw new SalesforceException(
+                "Component is configured with Salesforce Composite API format " + configuredFormat
+                    + ", but the payload of the Composite API operation requires format " + requiredFormat,
+                0);
+        }
+    }
+
+    static XStream configureXStream(final Class<?>... additionalTypes) {
+        final PureJavaReflectionProvider reflectionProvider = new PureJavaReflectionProvider(
+            new FieldDictionary(new AnnotationFieldKeySorter()));
+
+        final XppDriver hierarchicalStreamDriver = new XppDriver(new NoNameCoder()) {
+            @Override
+            public HierarchicalStreamWriter createWriter(final Writer out) {
+                return new CompactWriter(out, getNameCoder());
+            }
+
+        };
+
+        final XStream xStream = new XStream(reflectionProvider, hierarchicalStreamDriver);
+        xStream.aliasSystemAttribute(null, "class");
+        xStream.ignoreUnknownElements();
+        XStreamUtils.addDefaultPermissions(xStream);
+        xStream.registerConverter(new DateTimeConverter());
+        xStream.setMarshallingStrategy(new TreeMarshallingStrategy());
+        xStream.processAnnotations(additionalTypes);
+
+        return xStream;
+    }
+
+    static <T> T fromXml(final XStream xstream, final InputStream responseStream) {
+        @SuppressWarnings("unchecked")
+        final T read = (T) xstream.fromXML(responseStream);
+
+        return read;
+    }
+
+    static InputStream toXml(final XStream xstream, final Object obj) {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        xstream.toXML(obj, out);
+
+        return new ByteArrayInputStream(out.toByteArray());
+    }
+
 }
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/AbstractSalesforceProcessor.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/AbstractSalesforceProcessor.java
index 8f54431..cea51fa 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/AbstractSalesforceProcessor.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/AbstractSalesforceProcessor.java
@@ -38,7 +38,6 @@ public abstract class AbstractSalesforceProcessor implements SalesforceProcessor
     protected static final boolean USE_BODY = true;
     protected static final boolean IGNORE_BODY = false;
 
-
     protected final Logger log = LoggerFactory.getLogger(this.getClass());
 
     protected final SalesforceEndpoint endpoint;
@@ -47,24 +46,25 @@ public abstract class AbstractSalesforceProcessor implements SalesforceProcessor
     protected final OperationName operationName;
     protected final SalesforceSession session;
     protected final SalesforceHttpClient httpClient;
-    protected final boolean rawPayload; 
+    protected final boolean rawPayload;
 
-    public AbstractSalesforceProcessor(SalesforceEndpoint endpoint) {
+    public AbstractSalesforceProcessor(final SalesforceEndpoint endpoint) {
         this.endpoint = endpoint;
-        this.operationName = endpoint.getOperationName();
-        this.endpointConfigMap = endpoint.getConfiguration().toValueMap();
+        operationName = endpoint.getOperationName();
+        endpointConfigMap = endpoint.getConfiguration().toValueMap();
 
         final SalesforceComponent component = endpoint.getComponent();
-        this.session = component.getSession();
-        this.httpClient = endpoint.getConfiguration().getHttpClient();
-        this.rawPayload = endpoint.getConfiguration().getRawPayload();
+        session = component.getSession();
+        httpClient = endpoint.getConfiguration().getHttpClient();
+        rawPayload = endpoint.getConfiguration().getRawPayload();
     }
 
     @Override
     public abstract boolean process(Exchange exchange, AsyncCallback callback);
 
     /**
-     * Gets String value for a parameter from header, endpoint config, or exchange body (optional).
+     * Gets String value for a parameter from header, endpoint config, or
+     * exchange body (optional).
      *
      * @param exchange          exchange to inspect
      * @param convertInBody     converts In body to String value if true
@@ -74,7 +74,8 @@ public abstract class AbstractSalesforceProcessor implements SalesforceProcessor
      * @throws org.apache.camel.component.salesforce.api.SalesforceException
      *          if the property can't be found or on conversion errors.
      */
-    protected final String getParameter(String propName, Exchange exchange, boolean convertInBody, boolean optional) throws SalesforceException {
+    protected final String getParameter(final String propName, final Exchange exchange, final boolean convertInBody,
+        final boolean optional) throws SalesforceException {
         return getParameter(propName, exchange, convertInBody, optional, String.class);
     }
 
@@ -90,8 +91,8 @@ public abstract class AbstractSalesforceProcessor implements SalesforceProcessor
      * @throws org.apache.camel.component.salesforce.api.SalesforceException
      *          if the property can't be found or on conversion errors.
      */
-    protected final <T> T getParameter(String propName, Exchange exchange, boolean convertInBody, boolean optional,
-                                       Class<T> parameterClass) throws SalesforceException {
+    protected final <T> T getParameter(final String propName, final Exchange exchange, final boolean convertInBody,
+        final boolean optional, final Class<T> parameterClass) throws SalesforceException {
 
         final Message in = exchange.getIn();
         T propValue = in.getHeader(propName, parameterClass);
@@ -99,8 +100,8 @@ public abstract class AbstractSalesforceProcessor implements SalesforceProcessor
         if (propValue == null) {
             // check if type conversion failed
             if (in.getHeader(propName) != null) {
-                throw new IllegalArgumentException("Header " + propName
-                    + " could not be converted to type " + parameterClass.getName());
+                throw new IllegalArgumentException(
+                    "Header " + propName + " could not be converted to type " + parameterClass.getName());
             }
 
             final Object value = endpointConfigMap.get(propName);
@@ -111,17 +112,17 @@ public abstract class AbstractSalesforceProcessor implements SalesforceProcessor
 
                 try {
                     propValue = exchange.getContext().getTypeConverter().mandatoryConvertTo(parameterClass, value);
-                } catch (NoTypeConversionAvailableException e) {
+                } catch (final NoTypeConversionAvailableException e) {
                     throw new SalesforceException(e);
                 }
             }
         }
 
-        propValue = (propValue == null && convertInBody) ? in.getBody(parameterClass) : propValue;
+        propValue = propValue == null && convertInBody ? in.getBody(parameterClass) : propValue;
 
         // error if property was not set
         if (propValue == null && !optional) {
-            String msg = "Missing property " + propName
+            final String msg = "Missing property " + propName
                 + (convertInBody ? ", message body could not be converted to type " + parameterClass.getName() : "");
             throw new SalesforceException(msg, null);
         }
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/CompositeApiProcessor.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/CompositeApiProcessor.java
index 188e777..966a9b9 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/CompositeApiProcessor.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/CompositeApiProcessor.java
@@ -30,6 +30,8 @@ import org.apache.camel.component.salesforce.api.SalesforceException;
 import org.apache.camel.component.salesforce.api.dto.composite.ReferenceId;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatch;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResponse;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectComposite;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectCompositeResponse;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectTree;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectTreeResponse;
 import org.apache.camel.component.salesforce.internal.PayloadFormat;
@@ -42,7 +44,8 @@ public final class CompositeApiProcessor extends AbstractSalesforceProcessor {
     @FunctionalInterface
     interface ResponseHandler<T> {
 
-        void handleResponse(Exchange exchange, Optional<T> body, Map<String, String> headers, SalesforceException exception, AsyncCallback callback);
+        void handleResponse(Exchange exchange, Optional<T> body, Map<String, String> headers,
+            SalesforceException exception, AsyncCallback callback);
 
     }
 
@@ -76,6 +79,9 @@ public final class CompositeApiProcessor extends AbstractSalesforceProcessor {
             case COMPOSITE_BATCH:
                 return processInternal(SObjectBatch.class, exchange, compositeClient::submitCompositeBatch,
                     this::processCompositeBatchResponse, callback);
+            case COMPOSITE:
+                return processInternal(SObjectComposite.class, exchange, compositeClient::submitComposite,
+                    this::processCompositeResponse, callback);
             default:
                 throw new SalesforceException("Unknown operation name: " + operationName.value(), null);
             }
@@ -118,6 +124,26 @@ public final class CompositeApiProcessor extends AbstractSalesforceProcessor {
         }
     }
 
+    void processCompositeResponse(final Exchange exchange, final Optional<SObjectCompositeResponse> responseBody,
+        final Map<String, String> headers, final SalesforceException exception, final AsyncCallback callback) {
+        try {
+            if (!responseBody.isPresent()) {
+                exchange.setException(exception);
+            } else {
+                final Message in = exchange.getIn();
+                final Message out = exchange.getOut();
+
+                final SObjectCompositeResponse response = responseBody.get();
+
+                out.copyFromWithNewBody(in, response);
+                out.getHeaders().putAll(headers);
+            }
+        } finally {
+            // notify callback that exchange is done
+            callback.done(false);
+        }
+    }
+
     void processCompositeTreeResponse(final Exchange exchange, final Optional<SObjectTreeResponse> responseBody,
         final Map<String, String> headers, final SalesforceException exception, final AsyncCallback callback) {
 
@@ -158,13 +184,6 @@ public final class CompositeApiProcessor extends AbstractSalesforceProcessor {
         }
     }
 
-    boolean processException(final Exchange exchange, final AsyncCallback callback, final Exception e) {
-        exchange.setException(e);
-        callback.done(true);
-
-        return true;
-    }
-
     <T, R> boolean processInternal(final Class<T> bodyType, final Exchange exchange,
         final CompositeApiClient.Operation<T, R> clientOperation, final ResponseHandler<R> responseHandler,
         final AsyncCallback callback) throws SalesforceException {
@@ -178,10 +197,17 @@ public final class CompositeApiProcessor extends AbstractSalesforceProcessor {
             throw new SalesforceException(e);
         }
 
-        clientOperation.submit(body, determineHeaders(exchange),
-            (response, responseHeaders, exception) -> responseHandler.handleResponse(exchange, response, responseHeaders, exception, callback));
+        clientOperation.submit(body, determineHeaders(exchange), (response, responseHeaders,
+            exception) -> responseHandler.handleResponse(exchange, response, responseHeaders, exception, callback));
 
         return false;
     }
 
+    static boolean processException(final Exchange exchange, final AsyncCallback callback, final Exception e) {
+        exchange.setException(e);
+        callback.done(true);
+
+        return true;
+    }
+
 }
diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiBatchIntegrationTest.java b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiBatchIntegrationTest.java
index b70b87f..5bfadd0 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiBatchIntegrationTest.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiBatchIntegrationTest.java
@@ -129,6 +129,11 @@ public class CompositeApiBatchIntegrationTest extends AbstractSalesforceTestBase
         testBatch(batch);
     }
 
+    /**
+     * The XML format fails, as Salesforce API wrongly includes whitespaces
+     * inside tag names. E.g.  <Ant Migration Tool>
+     * https://www.w3.org/TR/2008/REC-xml-20081126/#NT-NameChar
+     */
     @Test
     public void shouldSupportLimits() {
         final SObjectBatch batch = new SObjectBatch(version);
diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiIntegrationTest.java b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiIntegrationTest.java
new file mode 100644
index 0000000..8f59cf8
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiIntegrationTest.java
@@ -0,0 +1,256 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.salesforce;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.googlecode.junittoolbox.ParallelParameterized;
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.salesforce.api.dto.AbstractQueryRecordsBase;
+import org.apache.camel.component.salesforce.api.dto.CreateSObjectResult;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectComposite;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectComposite.Method;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectCompositeResponse;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectCompositeResult;
+import org.apache.camel.component.salesforce.api.utils.Version;
+import org.apache.camel.component.salesforce.dto.generated.Account;
+import org.assertj.core.api.Assertions;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(ParallelParameterized.class)
+public class CompositeApiIntegrationTest extends AbstractSalesforceTestBase {
+
+    public static class Accounts extends AbstractQueryRecordsBase {
+        @XStreamImplicit
+        private List<Account> records;
+
+        public List<Account> getRecords() {
+            return records;
+        }
+
+        public void setRecords(final List<Account> records) {
+            this.records = records;
+        }
+
+    }
+
+    private static final Set<String> VERSIONS = new HashSet<>(Arrays.asList("38.0", "41.0"));
+
+    private String accountId;
+
+    private final String compositeUri;
+
+    private final String version;
+
+    public CompositeApiIntegrationTest(final String format, final String version) {
+        this.version = version;
+        compositeUri = "salesforce:composite?format=" + format;
+    }
+
+    @After
+    public void removeRecords() {
+        try {
+            template.sendBody("salesforce:deleteSObject?sObjectName=Account&sObjectId=" + accountId, null);
+        } catch (final CamelExecutionException ignored) {
+            // other tests run in parallel could have deleted the Account
+        }
+
+        template.request("direct:deleteBatchAccounts", null);
+    }
+
+    @Before
+    public void setupRecords() {
+        final Account account = new Account();
+        account.setName("Composite API Batch");
+
+        final CreateSObjectResult result = template.requestBody("salesforce:createSObject", account,
+            CreateSObjectResult.class);
+
+        accountId = result.getId();
+    }
+
+    @Test
+    public void shouldSubmitBatchUsingCompositeApi() {
+        final SObjectComposite composite = new SObjectComposite(version, true);
+
+        final Account updates = new Account();
+        updates.setName("NewName");
+        composite.addUpdate("Account", accountId, updates, "UpdateExistingAccountReferenceId");
+
+        final Account newAccount = new Account();
+        newAccount.setName("Account created from Composite batch API");
+        composite.addCreate(newAccount, "CreateAccountReferenceId");
+
+        composite.addGet("Account", accountId, "GetAccountReferenceId", "Name", "BillingPostalCode");
+
+        composite.addDelete("Account", accountId, "DeleteAccountReferenceId");
+
+        testComposite(composite);
+    }
+
+    @Test
+    public void shouldSupportGenericCompositeRequests() {
+        final SObjectComposite composite = new SObjectComposite(version, true);
+
+        composite.addGeneric(Method.GET, "/sobjects/Account/" + accountId, "GetExistingAccountReferenceId");
+
+        testComposite(composite);
+    }
+
+    @Test
+    public void shouldSupportObjectCreation() {
+        final SObjectComposite compoiste = new SObjectComposite(version, true);
+
+        final Account newAccount = new Account();
+        newAccount.setName("Account created from Composite batch API");
+        compoiste.addCreate(newAccount, "CreateAccountReferenceId");
+
+        final SObjectCompositeResponse response = testComposite(compoiste);
+
+        assertResponseContains(response, "id");
+    }
+
+    @Test
+    public void shouldSupportObjectDeletion() {
+        final SObjectComposite composite = new SObjectComposite(version, true);
+        composite.addDelete("Account", accountId, "DeleteAccountReferenceId");
+
+        testComposite(composite);
+    }
+
+    @Test
+    public void shouldSupportObjectRetrieval() {
+        final SObjectComposite composite = new SObjectComposite(version, true);
+
+        composite.addGet("Account", accountId, "GetExistingAccountReferenceId", "Name");
+
+        final SObjectCompositeResponse response = testComposite(composite);
+
+        assertResponseContains(response, "Name");
+    }
+
+    @Test
+    public void shouldSupportObjectUpdates() {
+        final SObjectComposite composite = new SObjectComposite(version, true);
+
+        final Account updates = new Account();
+        updates.setName("NewName");
+        updates.setAccountNumber("AC12345");
+        composite.addUpdate("Account", accountId, updates, "UpdateAccountReferenceId");
+
+        testComposite(composite);
+    }
+
+    @Test
+    public void shouldSupportQuery() {
+        final SObjectComposite composite = new SObjectComposite(version, true);
+        composite.addQuery("SELECT Id, Name FROM Account", "SelectQueryReferenceId");
+
+        final SObjectCompositeResponse response = testComposite(composite);
+
+        assertResponseContains(response, "totalSize");
+    }
+
+    @Test
+    public void shouldSupportQueryAll() {
+        final SObjectComposite composite = new SObjectComposite(version, true);
+        composite.addQueryAll("SELECT Id, Name FROM Account", "SelectQueryReferenceId");
+
+        final SObjectCompositeResponse response = testComposite(composite);
+
+        assertResponseContains(response, "totalSize");
+    }
+
+    @Test
+    public void shouldSupportRelatedObjectRetrieval() {
+        if (Version.create(version).compareTo(Version.create("36.0")) < 0) {
+            return;
+        }
+
+        final SObjectComposite composite = new SObjectComposite("36.0", true);
+        composite.addGetRelated("Account", accountId, "CreatedBy", "GetRelatedAccountReferenceId");
+
+        final SObjectCompositeResponse response = testComposite(composite);
+
+        assertResponseContains(response, "Username");
+    }
+
+    SObjectCompositeResponse testComposite(final SObjectComposite batch) {
+        final SObjectCompositeResponse response = template.requestBody(compositeUri, batch, SObjectCompositeResponse.class);
+
+        Assertions.assertThat(response).as("Response should be provided").isNotNull();
+
+        Assertions.assertThat(response.getCompositeResponse()).as("Received errors in: " + response)
+            .allMatch(val -> val.getHttpStatusCode() >= 200 && val.getHttpStatusCode() <= 299);
+
+        return response;
+    }
+
+    @Override
+    protected RouteBuilder doCreateRouteBuilder() throws Exception {
+        return new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                from("direct:deleteBatchAccounts")
+                    .to("salesforce:query?sObjectClass=" + Accounts.class.getName()
+                        + "&sObjectQuery=SELECT Id FROM Account WHERE Name = 'Account created from Composite batch API'")
+                    .split(simple("${body.records}")).setHeader("sObjectId", simple("${body.id}"))
+                    .to("salesforce:deleteSObject?sObjectName=Account").end();
+            }
+        };
+    }
+
+    @Override
+    protected String salesforceApiVersionToUse() {
+        return version;
+    }
+
+    @Parameters(name = "format = {0}, version = {1}")
+    public static Iterable<Object[]> formats() {
+        return VERSIONS.stream().map(v -> new Object[] {"JSON", v}).collect(Collectors.toList());
+    }
+
+    static void assertResponseContains(final SObjectCompositeResponse response, final String key) {
+        Assertions.assertThat(response).isNotNull();
+
+        final List<SObjectCompositeResult> compositeResponse = response.getCompositeResponse();
+        Assertions.assertThat(compositeResponse).hasSize(1);
+
+        final SObjectCompositeResult firstCompositeResponse = compositeResponse.get(0);
+        Assertions.assertThat(firstCompositeResponse).isNotNull();
+
+        final Object firstCompositeResponseBody = firstCompositeResponse.getBody();
+        Assertions.assertThat(firstCompositeResponseBody).isInstanceOf(Map.class);
+
+        @SuppressWarnings("unchecked")
+        final Map<String, ?> body = (Map<String, ?>) firstCompositeResponseBody;
+        Assertions.assertThat(body).containsKey(key);
+        Assertions.assertThat(body.get(key)).isNotNull();
+    }
+}
diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeResponseTest.java b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeResponseTest.java
new file mode 100644
index 0000000..d89e2c7
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeResponseTest.java
@@ -0,0 +1,139 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.salesforce.api.dto.composite;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SObjectCompositeResponseTest {
+
+    @Test
+    public void shouldDeserializeFailedJsonResponse() throws IOException {
+
+        final String json = IOUtils.toString(
+            this.getClass().getResourceAsStream(
+                "/org/apache/camel/component/salesforce/api/dto/composite_response_example_failure.json"),
+            Charset.forName("UTF-8"));
+
+        final ObjectMapper mapper = new ObjectMapper();
+
+        final SObjectCompositeResponse response = mapper.readerFor(SObjectCompositeResponse.class).readValue(json);
+
+        assertFailedResponse(response);
+    }
+
+    @Test
+    public void shouldDeserializeSuccessfulJsonResponse() throws IOException {
+
+        final String json = IOUtils.toString(
+            this.getClass().getResourceAsStream(
+                "/org/apache/camel/component/salesforce/api/dto/composite_response_example_success.json"),
+            Charset.forName("UTF-8"));
+
+        final ObjectMapper mapper = new ObjectMapper();
+
+        final SObjectCompositeResponse response = mapper.readerFor(SObjectCompositeResponse.class).readValue(json);
+
+        assertSuccessfulResponse(response);
+    }
+
+    static void assertFailedResponse(final SObjectCompositeResponse response) {
+        final List<SObjectCompositeResult> compositeResponse = response.getCompositeResponse();
+        final List<SObjectCompositeResult> results = compositeResponse;
+        assertThat(results).as("It should contain 2 results").hasSize(2);
+
+        // upsert
+        final SObjectCompositeResult upsertResponse = compositeResponse.get(0);
+        assertThat(upsertResponse.getReferenceId()).as("ReferenceId of first operation should be NewPayment1")
+            .isEqualTo("NewPayment1");
+        assertThat(upsertResponse.getHttpStatusCode()).as("httpStatusCode of first operation should be 400")
+            .isEqualTo(400);
+        assertThat(upsertResponse.getBody()).isInstanceOf(List.class);
+        @SuppressWarnings("unchecked")
+        final List<Map<String, Object>> upsertBody = (List<Map<String, Object>>) upsertResponse.getBody();
+        assertThat(upsertBody).hasSize(1);
+        final Map<String, Object> upsertBodyContent = upsertBody.get(0);
+        assertThat(upsertBodyContent).as("message of the create operation should be populated properly").containsEntry(
+            "message", "The transaction was rolled back since another operation in the same transaction failed.");
+        assertThat(upsertBodyContent).as("errorCode of the create operation should be PROCESSING_HALTED")
+            .containsEntry("errorCode", "PROCESSING_HALTED");
+
+        // create
+        final SObjectCompositeResult createResponse = compositeResponse.get(1);
+        assertThat(createResponse.getReferenceId()).as("ReferenceId of first operation should be NewPayment2")
+            .isEqualTo("NewPayment2");
+        assertThat(createResponse.getHttpStatusCode()).as("httpStatusCode of first operation should be 400")
+            .isEqualTo(400);
+        @SuppressWarnings("unchecked")
+        final List<Map<String, Object>> createBody = (List<Map<String, Object>>) createResponse.getBody();
+        assertThat(createBody).hasSize(1);
+        final Map<String, Object> createBodyContent = createBody.get(0);
+        assertThat(createBodyContent).as("message of the create operation should be populated properly").containsEntry(
+            "message",
+            "Foreign key external ID: 0116 not found for field Invoice_External_Id__c in entity blng__Invoice__c");
+        assertThat(createBodyContent).as("errorCode of the create operation should be INVALID_FIELD")
+            .containsEntry("errorCode", "INVALID_FIELD");
+    }
+
+    static void assertSuccessfulResponse(final SObjectCompositeResponse response) {
+
+        final List<SObjectCompositeResult> compositeResponse = response.getCompositeResponse();
+        final List<SObjectCompositeResult> results = compositeResponse;
+        assertThat(results).as("It should contain 2 results").hasSize(2);
+
+        // create 1
+        final SObjectCompositeResult firstResponse = compositeResponse.get(0);
+        assertThat(firstResponse.getHttpHeaders()).as("Location of the create resource should be populated")
+            .containsEntry("Location", "/services/data/v41.0/sobjects/blng__Payment__c/a1V3E000000EXomUAM");
+        assertThat(firstResponse.getHttpStatusCode()).as("httpStatusCode of the create operation should be 201")
+            .isEqualTo(201);
+        assertThat(firstResponse.getReferenceId()).as("ReferenceId of the create operation should be NewPayment1")
+            .isEqualTo("NewPayment1");
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> firstResponseMap = (Map<String, Object>) firstResponse.getBody();
+        assertThat(firstResponseMap).as("id of the create operation should be a1V3E000000EXomUAM").containsEntry("id",
+            "a1V3E000000EXomUAM");
+        assertThat(firstResponseMap).as("success of the create operation should be true").containsEntry("success",
+            Boolean.TRUE);
+
+        // create 2
+        final SObjectCompositeResult secondResponse = compositeResponse.get(1);
+        assertThat(secondResponse.getHttpHeaders()).as("Location of the create resource should be populated")
+            .containsEntry("Location", "/services/data/v41.0/sobjects/blng__Payment__c/a1V3E000000EXomUAG");
+        assertThat(secondResponse.getHttpStatusCode()).as("httpStatusCode of the create operation should be 201")
+            .isEqualTo(201);
+        assertThat(secondResponse.getReferenceId()).as("ReferenceId of the create operation should be NewPayment2")
+            .isEqualTo("NewPayment2");
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> secondResponseMap = (Map<String, Object>) secondResponse.getBody();
+        assertThat(secondResponseMap).as("id of the create operation should be a1V3E000000EXomUAG").containsEntry("id",
+            "a1V3E000000EXomUAG");
+        assertThat(secondResponseMap).as("success of the create operation should be true").containsEntry("success",
+            Boolean.TRUE);
+    }
+
+}
diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeTest.java b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeTest.java
new file mode 100644
index 0000000..5345875
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectCompositeTest.java
@@ -0,0 +1,119 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.salesforce.api.dto.composite;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+import org.apache.camel.component.salesforce.api.dto.AbstractDescribedSObjectBase;
+import org.apache.camel.component.salesforce.api.dto.SObjectDescription;
+import org.apache.camel.component.salesforce.dto.generated.Account;
+import org.apache.camel.component.salesforce.dto.generated.Account_IndustryEnum;
+import org.apache.camel.component.salesforce.dto.generated.Contact;
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SObjectCompositeTest {
+
+    // CHECKSTYLE:OFF
+    @JsonPropertyOrder({"account__c", "contactId__c"})
+    public static class AccountContactJunction__c extends AbstractDescribedSObjectBase {
+
+        private String account__c;
+
+        private String contactId__c;
+
+        @Override
+        public SObjectDescription description() {
+            return new SObjectDescription();
+        }
+
+        public String getAccount__c() {
+            return account__c;
+        }
+
+        public String getContactId__c() {
+            return contactId__c;
+        }
+
+        public void setAccount__c(final String account__c) {
+            this.account__c = account__c;
+        }
+
+        public void setContactId__c(final String contactId__c) {
+            this.contactId__c = contactId__c;
+        }
+    }
+    // CHECKSTYLE:ON
+
+    @JsonPropertyOrder({"Name", "BillingStreet", "BillingCity", "BillingState", "Industry"})
+    public static class TestAccount extends Account {
+        // just for property order
+    }
+
+    @JsonPropertyOrder({"LastName", "Phone"})
+    public static class TestContact extends Contact {
+        // just for property order
+    }
+
+    private final SObjectComposite composite;
+
+    public SObjectCompositeTest() {
+        composite = new SObjectComposite("38.0", true);
+
+        // first insert operation via an external id
+        final Account updateAccount = new TestAccount();
+        updateAccount.setName("Salesforce");
+        updateAccount.setBillingStreet("Landmark @ 1 Market Street");
+        updateAccount.setBillingCity("San Francisco");
+        updateAccount.setBillingState("California");
+        updateAccount.setIndustry(Account_IndustryEnum.TECHNOLOGY);
+        composite.addUpdate("Account", "001xx000003DIpcAAG", updateAccount, "UpdatedAccount");
+
+        final Contact newContact = new TestContact();
+        newContact.setLastName("John Doe");
+        newContact.setPhone("1234567890");
+        composite.addCreate(newContact, "NewContact");
+
+        final AccountContactJunction__c junction = new AccountContactJunction__c();
+        junction.setAccount__c("001xx000003DIpcAAG");
+        junction.setContactId__c("@{NewContact.id}");
+        composite.addCreate(junction, "JunctionRecord");
+    }
+
+    @Test
+    public void shouldSerializeToJson() throws IOException {
+
+        final String expectedJson = IOUtils.toString(
+            SObjectCompositeTest.class
+                .getResourceAsStream("/org/apache/camel/component/salesforce/api/dto/composite_request_example.json"),
+            StandardCharsets.UTF_8);
+
+        final ObjectMapper mapper = new ObjectMapper().configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
+            .configure(SerializationFeature.INDENT_OUTPUT, true);
+
+        final String serialized = mapper.writerFor(SObjectComposite.class).writeValueAsString(composite);
+
+        assertThat(serialized).as("Should serialize as expected by Salesforce").isEqualTo(expectedJson);
+    }
+}
diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/resources/org/apache/camel/component/salesforce/api/dto/composite_request_example.json b/components/camel-salesforce/camel-salesforce-component/src/test/resources/org/apache/camel/component/salesforce/api/dto/composite_request_example.json
new file mode 100644
index 0000000..513e352
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/resources/org/apache/camel/component/salesforce/api/dto/composite_request_example.json
@@ -0,0 +1,31 @@
+{
+  "allOrNone" : true,
+  "compositeRequest" : [ {
+    "method" : "PATCH",
+    "url" : "/services/data/v38.0/sobjects/Account/001xx000003DIpcAAG",
+    "referenceId" : "UpdatedAccount",
+    "body" : {
+      "Name" : "Salesforce",
+      "BillingStreet" : "Landmark @ 1 Market Street",
+      "BillingCity" : "San Francisco",
+      "BillingState" : "California",
+      "Industry" : "Technology"
+    }
+  }, {
+    "method" : "POST",
+    "url" : "/services/data/v38.0/sobjects/Contact/",
+    "referenceId" : "NewContact",
+    "body" : {
+      "LastName" : "John Doe",
+      "Phone" : "1234567890"
+    }
+  }, {
+    "method" : "POST",
+    "url" : "/services/data/v38.0/sobjects/null/",
+    "referenceId" : "JunctionRecord",
+    "body" : {
+      "account__c" : "001xx000003DIpcAAG",
+      "contactId__c" : "@{NewContact.id}"
+    }
+  } ]
+}
\ No newline at end of file
diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/resources/org/apache/camel/component/salesforce/api/dto/composite_response_example_failure.json b/components/camel-salesforce/camel-salesforce-component/src/test/resources/org/apache/camel/component/salesforce/api/dto/composite_response_example_failure.json
new file mode 100644
index 0000000..439e9f2
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/resources/org/apache/camel/component/salesforce/api/dto/composite_response_example_failure.json
@@ -0,0 +1,20 @@
+{
+  "compositeResponse" : [ {
+    "body" : [ {
+      "errorCode" : "PROCESSING_HALTED",
+      "message" : "The transaction was rolled back since another operation in the same transaction failed."
+    } ],
+    "httpHeaders" : { },
+    "httpStatusCode" : 400,
+    "referenceId" : "NewPayment1"
+  }, {
+    "body" : [ {
+      "message" : "Foreign key external ID: 0116 not found for field Invoice_External_Id__c in entity blng__Invoice__c",
+      "errorCode" : "INVALID_FIELD",
+      "fields" : [ ]
+    } ],
+    "httpHeaders" : { },
+    "httpStatusCode" : 400,
+    "referenceId" : "NewPayment2"
+  } ]
+}
\ No newline at end of file
diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/resources/org/apache/camel/component/salesforce/api/dto/composite_response_example_success.json b/components/camel-salesforce/camel-salesforce-component/src/test/resources/org/apache/camel/component/salesforce/api/dto/composite_response_example_success.json
new file mode 100644
index 0000000..4af33bf
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/resources/org/apache/camel/component/salesforce/api/dto/composite_response_example_success.json
@@ -0,0 +1,27 @@
+{
+  "compositeResponse" : [ {
+    "body" : {
+      "id" : "a1V3E000000EXomUAM",
+      "success" : true,
+      "errors" : [ ],
+      "warnings" : [ ]
+    },
+    "httpHeaders" : {
+      "Location" : "/services/data/v41.0/sobjects/blng__Payment__c/a1V3E000000EXomUAM"
+    },
+    "httpStatusCode" : 201,
+    "referenceId" : "NewPayment1"
+  }, {
+    "body" : {
+      "id" : "a1V3E000000EXomUAG",
+      "success" : true,
+      "errors" : [ ],
+      "warnings" : [ ]
+    },
+    "httpHeaders" : {
+      "Location" : "/services/data/v41.0/sobjects/blng__Payment__c/a1V3E000000EXomUAG"
+    },
+    "httpStatusCode" : 201,
+    "referenceId" : "NewPayment2"
+  } ]
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
['"commits@camel.apache.org" <co...@camel.apache.org>'].