You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2016/12/14 10:57:10 UTC

[4/4] camel git commit: CAMEL-10571 SObject tree creation, Composite API

CAMEL-10571 SObject tree creation, Composite API

This commit implements support for SObject tree creation via Salesforce
Composite API. This allows user to create multiple, up to 200, records
including parent-child relationships, up to 5 levels using one API call.

For instance:

Account account = ...
Contact president = ...
Contact marketing = ...

Account anotherAccount = ...
Contact sales = ...
Asset someAsset = ...

// build the tree
SObjectTree request = new SObjectTree();
request.addObject(account).addChildren(president, marketing);
request.addObject(anotherAccount).addChild(sales).addChild(someAsset);

final SObjectTree response =
template.requestBody("salesforce:composite-tree", tree,
SObjectTree.class);

The returned response in the output message is the identical object
received in the body of the input message updated with identifiers
received on successful record creation or populated errors on failure.


Project: http://git-wip-us.apache.org/repos/asf/camel/repo
Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/e1cfeb5a
Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/e1cfeb5a
Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/e1cfeb5a

Branch: refs/heads/master
Commit: e1cfeb5a55200350771c88c93e65720990131610
Parents: a524de1
Author: Zoran Regvart <zo...@regvart.com>
Authored: Tue Dec 13 21:05:15 2016 +0100
Committer: Claus Ibsen <da...@apache.org>
Committed: Wed Dec 14 11:54:16 2016 +0100

----------------------------------------------------------------------
 .../src/main/docs/salesforce-component.adoc     |   46 +
 .../salesforce/SalesforceEndpointConfig.java    |    2 +-
 .../salesforce/SalesforceProducer.java          |   12 +
 .../component/salesforce/api/dto/RestError.java |   36 +
 .../api/dto/composite/Attributes.java           |   53 +
 .../salesforce/api/dto/composite/Counter.java   |   33 +
 .../api/dto/composite/ReferenceGenerator.java   |   41 +
 .../api/dto/composite/ReferenceId.java          |  107 +
 .../api/dto/composite/SObjectNode.java          |  302 ++
 .../composite/SObjectNodeXStreamConverter.java  |   59 +
 .../api/dto/composite/SObjectTree.java          |  294 ++
 .../api/dto/composite/SObjectTreeResponse.java  |   64 +
 .../salesforce/internal/OperationName.java      |    5 +-
 .../internal/client/AbstractClientBase.java     |    4 +-
 .../internal/client/CompositeApiClient.java     |   51 +
 .../client/DefaultCompositeApiClient.java       |  230 ++
 .../processor/CompositeApiProcessor.java        |  160 +
 .../CompositeApiTreeIntegrationTest.java        |  117 +
 ...ceComponentConfigurationIntegrationTest.java |    8 +-
 .../salesforce/api/dto/RestErrorTest.java       |   61 +
 .../api/dto/composite/CompositeTestBase.java    |   66 +
 .../api/dto/composite/SObjectNodeTest.java      |  199 ++
 .../dto/composite/SObjectTreeResponseTest.java  |  171 +
 .../api/dto/composite/SObjectTreeTest.java      |  291 ++
 .../salesforce/dto/generated/Account.java       | 3188 +++++++++++++++++-
 .../dto/generated/Account_IndustryEnum.java     |  119 +
 .../salesforce/dto/generated/Asset.java         |  177 +
 .../salesforce/dto/generated/Contact.java       |  596 ++++
 .../camel-salesforce-maven-plugin/.gitignore    |    1 +
 29 files changed, 6481 insertions(+), 12 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/docs/salesforce-component.adoc
----------------------------------------------------------------------
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 53f0f81..e36dcbb 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
@@ -71,6 +71,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-tree - create up to 200 records with parent-child relationships (up to 5 levels) in one go
 
 For example, the following producer endpoint uses the upsertSObject API,
 with the sObjectIdName parameter specifying 'Name' as the external id
@@ -219,6 +220,7 @@ from("direct:querySalesforce")
     .otherwise()
         .setBody(constant("Used up Salesforce API limits, leaving 10% for critical routes"))
     .endChoice()
+-----------------------------------------------------------------------------------------------------
 
 [[Salesforce-Approval]]
 Working with approvals
@@ -330,6 +332,50 @@ body.put("nextApproverIds", userId);
 final ApprovalResult result = template.requestBody("direct:example1", body, ApprovalResult.class);
 -----------------------------------------------------------------------------------------------------
 
+[[Salesforce-CompositeAPI-Tree]]
+Using Salesforce Composite API to submit SObject tree
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To create up to 200 records including parent-child relationships use `salesforce:composite-tree` operation. This
+requires an instance of `org.apache.camel.component.salesforce.api.dto.composite.SObjectTree` in the input 
+message and returns the same tree of objects in the output message. The 
+`org.apache.camel.component.salesforce.api.dto.AbstractSObjectBase` instances within the tree get updated with
+the identifier values (`Id` property) or their corresponding
+`org.apache.camel.component.salesforce.api.dto.composite.SObjectNode` is populated with `errors` on failure.
+
+Note that for some records operation can succeed and for some it can fail -- so you need to manually check for errors.
+
+Easiest way to use this functionality is to use the DTOs generated by the `camel-salesforce-maven-plugin`, but you
+also have the option of customizing the references that identify the each object in the tree, for instance primary keys
+from your database.
+
+Lets look at an example:
+
+[source,java]
+-----------------------------------------------------------------------------------------------------
+Account account = ...
+Contact president = ...
+Contact marketing = ...
+
+Account anotherAccount = ...
+Contact sales = ...
+Asset someAsset = ...
+
+// build the tree
+SObjectTree request = new SObjectTree();
+request.addObject(account).addChildren(president, marketing);
+request.addObject(anotherAccount).addChild(sales).addChild(someAsset);
+
+final SObjectTree response = template.requestBody("salesforce:composite-tree", tree, SObjectTree.class);
+final Map<Boolean, List<SObjectNode>> result = response.allNodes()
+                                                   .collect(Collectors.groupingBy(SObjectNode::hasErrors));
+
+final List<SObjectNode> withErrors = result.get(true);
+final List<SObjectNode> succeeded = result.get(false);
+
+final String firstId = succeeded.get(0).getId();
+-----------------------------------------------------------------------------------------------------
+
 [[Salesforce-CamelSalesforceMavenPlugin]]
 Camel Salesforce Maven Plugin
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpointConfig.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpointConfig.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpointConfig.java
index e0ac30f..99c9098 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpointConfig.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpointConfig.java
@@ -41,7 +41,7 @@ import org.apache.camel.spi.UriParams;
 public class SalesforceEndpointConfig implements Cloneable {
 
     // default API version
-    public static final String DEFAULT_VERSION = "33.0";
+    public static final String DEFAULT_VERSION = "34.0";
 
     // general parameter
     public static final String API_VERSION = "apiVersion";

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceProducer.java
----------------------------------------------------------------------
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 55b1bf0..ed68813 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
@@ -23,6 +23,7 @@ import org.apache.camel.component.salesforce.internal.OperationName;
 import org.apache.camel.component.salesforce.internal.PayloadFormat;
 import org.apache.camel.component.salesforce.internal.processor.AnalyticsApiProcessor;
 import org.apache.camel.component.salesforce.internal.processor.BulkApiProcessor;
+import org.apache.camel.component.salesforce.internal.processor.CompositeApiProcessor;
 import org.apache.camel.component.salesforce.internal.processor.JsonRestProcessor;
 import org.apache.camel.component.salesforce.internal.processor.SalesforceProcessor;
 import org.apache.camel.component.salesforce.internal.processor.XmlRestProcessor;
@@ -48,6 +49,8 @@ public class SalesforceProducer extends DefaultAsyncProducer {
             processor = new BulkApiProcessor(endpoint);
         } else if (isAnalyticsOperation(operationName)) {
             processor = new AnalyticsApiProcessor(endpoint);
+        } else if (isCompositeOperation(operationName)) {
+            processor = new CompositeApiProcessor(endpoint);
         } else {
             // create an appropriate processor
             if (payloadFormat == PayloadFormat.JSON) {
@@ -93,6 +96,15 @@ public class SalesforceProducer extends DefaultAsyncProducer {
         }
     }
 
+    private boolean isCompositeOperation(OperationName operationName) {
+        switch (operationName) {
+        case COMPOSITE_TREE:
+            return true;
+        default:
+            return false;
+        }
+    }
+
     @Override
     public boolean process(Exchange exchange, AsyncCallback callback) {
         log.debug("Processing {}",

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/RestError.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/RestError.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/RestError.java
index c67cf07..74fbcbb 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/RestError.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/RestError.java
@@ -16,12 +16,18 @@
  */
 package org.apache.camel.component.salesforce.api.dto;
 
+import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSetter;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
 import com.thoughtworks.xstream.annotations.XStreamImplicit;
 //CHECKSTYLE:OFF
 public class RestError extends AbstractDTOBase {
 
+    @XStreamAlias("statusCode")
     private String errorCode;
     private String message;
     @XStreamImplicit
@@ -65,5 +71,35 @@ public class RestError extends AbstractDTOBase {
         this.fields = fields;
     }
 
+    @JsonSetter("statusCode")
+    void setStatusCode(final String statusCode) {
+        errorCode = statusCode;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof RestError)) {
+            return false;
+        }
+
+        final RestError other = (RestError) obj;
+
+        return Objects.equals(errorCode, other.errorCode) && Objects.equals(message, other.message) && Objects.equals(fields, other.fields);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((errorCode == null) ? 0 : errorCode.hashCode());
+        result = prime * result + ((fields == null) ? 0 : fields.hashCode());
+        result = prime * result + ((message == null) ? 0 : message.hashCode());
+        return result;
+    }
+
 }
 //CHECKSTYLE:ON

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/Attributes.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/Attributes.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/Attributes.java
new file mode 100644
index 0000000..6aa32ba
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/Attributes.java
@@ -0,0 +1,53 @@
+/**
+ * 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.Objects;
+
+import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
+
+/**
+ * Holds {@code type} and {@code referrenceId} attributes needed for the SObject tree Composite API.
+ */
+final class Attributes implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @XStreamAsAttribute
+    final String type;
+
+    @XStreamAsAttribute
+    final String referenceId;
+
+    Attributes(String referenceId, final String type) {
+        this.type = Objects.requireNonNull(type, "Type must be specified");
+        this.referenceId = Objects.requireNonNull(referenceId, "Reference ID must be specified");
+    }
+
+    public String getReferenceId() {
+        return referenceId;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    @Override
+    public String toString() {
+        return "Attribute<type: " + type + ", referenceId: " + referenceId + ">";
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/Counter.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/Counter.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/Counter.java
new file mode 100644
index 0000000..3e33990
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/Counter.java
@@ -0,0 +1,33 @@
+/**
+ * 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.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Default {@link ReferenceGenerator} implementation that generates a counted sequence of references (ref1, ref2, ...).
+ */
+public class Counter implements ReferenceGenerator {
+
+    private final AtomicInteger counter = new AtomicInteger(0);
+
+    @Override
+    public String nextReferenceFor(final Object object) {
+        return "ref" + counter.incrementAndGet();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/ReferenceGenerator.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/ReferenceGenerator.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/ReferenceGenerator.java
new file mode 100644
index 0000000..e14899b
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/ReferenceGenerator.java
@@ -0,0 +1,41 @@
+/**
+ * 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;
+
+/**
+ * SObject tree Composite API interface for {@code referenceId} generation. For each object given to the
+ * {@link ReferenceGenerator#nextReferenceFor(Object)} method, the implementation should generate reference identifiers.
+ * Reference identifiers need to be unique within one SObject tree request and should start with alphanumeric character.
+ * <p/>
+ * For example you can provide your {@link ReferenceGenerator} implementation that uses identities within your own
+ * system as references, i.e. primary keys of records in your database.
+ *
+ * @see Counter
+ */
+public interface ReferenceGenerator {
+
+    /**
+     * Generates unique, within a request, reference identifier for the given object. Reference identifier must start
+     * with an alphanumeric.
+     *
+     * @param object
+     *            object to generate reference identifier for
+     * @return generated reference identifier
+     */
+    String nextReferenceFor(Object object);
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/ReferenceId.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/ReferenceId.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/ReferenceId.java
new file mode 100644
index 0000000..2cb970d
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/ReferenceId.java
@@ -0,0 +1,107 @@
+/**
+ * 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.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableList;
+import static java.util.Optional.ofNullable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
+
+import org.apache.camel.component.salesforce.api.dto.RestError;
+
+@XStreamAlias("results")
+public final class ReferenceId implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @XStreamImplicit
+    private final List<RestError> errors;
+
+    private final String id;
+
+    private final String referenceId;
+
+    @JsonCreator
+    ReferenceId(@JsonProperty("referenceId") final String referenceId, @JsonProperty("id") final String id,
+            @JsonProperty("errors") final List<RestError> errors) {
+        this.referenceId = referenceId;
+        this.id = id;
+        this.errors = errors;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof ReferenceId)) {
+            return false;
+        }
+
+        final ReferenceId other = (ReferenceId) obj;
+
+        return Objects.equals(id, other.id) && Objects.equals(referenceId, other.referenceId)
+            && Objects.equals(getErrors(), other.getErrors());
+    }
+
+    public List<RestError> getErrors() {
+        return unmodifiableList(ofNullable(errors).orElse(emptyList()));
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public String getReferenceId() {
+        return referenceId;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + (errors == null ? 0 : errors.hashCode());
+        result = prime * result + (id == null ? 0 : id.hashCode());
+        result = prime * result + (referenceId == null ? 0 : referenceId.hashCode());
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder buildy = new StringBuilder("Reference: ").append(referenceId).append(", Id: ");
+
+        final List<RestError> anyErrors = getErrors();
+        if (anyErrors.isEmpty()) {
+            buildy.append(", with no errors");
+        } else {
+            buildy.append(", with ");
+            buildy.append(anyErrors.size());
+            buildy.append(" error(s)");
+        }
+
+        return buildy.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectNode.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectNode.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectNode.java
new file mode 100644
index 0000000..9edfcfe
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectNode.java
@@ -0,0 +1,302 @@
+/**
+ * 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.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.util.Objects.requireNonNull;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamConverter;
+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.dto.RestError;
+import org.apache.camel.component.salesforce.api.dto.SObjectDescription;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * Represents one node in the SObject tree request. SObject trees ({@link SObjectTree}) are composed from instances of
+ * {@link SObjectNode}s. Each {@link SObjectNode} contains {@link Attributes}, the SObject ({@link AbstractSObjectBase})
+ * and any child records linked to it. SObjects at root level are added to {@link SObjectTree} using
+ * {@link SObjectTree#addObject(AbstractSObjectBase)}, then you can add child records on the {@link SObjectNode}
+ * returned by using {@link #addChild(AbstractDescribedSObjectBase)},
+ * {@link #addChildren(AbstractDescribedSObjectBase, AbstractDescribedSObjectBase...)} or
+ * {@link #addChild(String, AbstractSObjectBase)} and
+ * {@link #addChildren(String, AbstractSObjectBase, AbstractSObjectBase...)}.
+ * <p/>
+ * Upon submission to the Salesforce Composite API the {@link SObjectTree} and the {@link SObjectNode}s in it might
+ * contain errors that you need to fetch using {@link #getErrors()} method.
+ *
+ * @see SObjectTree
+ * @see RestError
+ */
+@XStreamAlias("records")
+@XStreamConverter(SObjectNodeXStreamConverter.class)
+public final class SObjectNode implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonProperty
+    final Attributes attributes;
+
+    @JsonUnwrapped
+    final AbstractSObjectBase object;
+
+    final Map<String, List<SObjectNode>> records = new HashMap<>();
+
+    private List<RestError> errors;
+
+    @XStreamOmitField
+    private final ReferenceGenerator referenceGenerator;
+
+    SObjectNode(final SObjectTree tree, final AbstractSObjectBase object) {
+        this(tree.referenceGenerator, typeOf(object), object);
+    }
+
+    private SObjectNode(final ReferenceGenerator referenceGenerator, final String type,
+            final AbstractSObjectBase object) {
+        this.referenceGenerator = requireNonNull(referenceGenerator, "ReferenceGenerator cannot be null");
+        this.object = requireNonNull(object, "Root SObject cannot be null");
+        attributes = new Attributes(referenceGenerator.nextReferenceFor(object),
+            requireNonNull(type, "Object type cannot be null"));
+    }
+
+    static String pluralOf(final AbstractDescribedSObjectBase object) {
+        final SObjectDescription description = object.description();
+
+        return description.getLabelPlural();
+    }
+
+    static String typeOf(final AbstractDescribedSObjectBase object) {
+        final SObjectDescription description = object.description();
+        return description.getName();
+    }
+
+    static String typeOf(final AbstractSObjectBase object) {
+        return object.getClass().getSimpleName();
+    }
+
+    /**
+     * Add a described child with the metadata needed already present within it to the this node.
+     *
+     * @param child
+     *            to add
+     * @return the newly created node, used in builder fashion to add more child objects to it (on the next level)
+     */
+    public SObjectNode addChild(final AbstractDescribedSObjectBase child) {
+        ObjectHelper.notNull(child, "child");
+
+        return addChild(pluralOf(child), child);
+    }
+
+    /**
+     * Add a child that does not contain the required metadata to the this node. You need to specify the plural form of
+     * the child (e.g. `Account` its `Accounts`).
+     *
+     * @param labelPlural
+     *            plural form
+     * @param child
+     *            to add
+     * @return the newly created node, used in builder fashion to add more child objects to it (on the next level)
+     */
+    public SObjectNode addChild(final String labelPlural, final AbstractSObjectBase child) {
+        ObjectHelper.notNull(labelPlural, "labelPlural");
+        ObjectHelper.notNull(child, "child");
+
+        final SObjectNode node = new SObjectNode(referenceGenerator, typeOf(child), child);
+
+        return addChild(labelPlural, node);
+    }
+
+    /**
+     * Add multiple described children with the metadata needed already present within them to the this node..
+     *
+     * @param first
+     *            first child to add
+     * @param others
+     *            any other children to add
+     */
+    public void addChildren(final AbstractDescribedSObjectBase first, final AbstractDescribedSObjectBase... others) {
+        ObjectHelper.notNull(first, "first");
+        ObjectHelper.notNull(others, "others");
+
+        addChild(pluralOf(first), first);
+
+        Arrays.stream(others).forEach(this::addChild);
+    }
+
+    /**
+     * Add a child that does not contain the required metadata to the this node. You need to specify the plural form of
+     * the child (e.g. `Account` its `Accounts`).
+     *
+     * @param labelPlural
+     *            plural form
+     * @param first
+     *            first child to add
+     * @param others
+     *            any other children to add
+     */
+    public void addChildren(final String labelPlural, final AbstractSObjectBase first,
+        final AbstractSObjectBase... others) {
+        ObjectHelper.notNull(labelPlural, "labelPlural");
+        ObjectHelper.notNull(first, "first");
+        ObjectHelper.notNull(others, "others");
+
+        addChild(labelPlural, first);
+
+        Arrays.stream(others).forEach(c -> addChild(labelPlural, c));
+    }
+
+    /**
+     * Returns all children of this node (one level deep).
+     *
+     * @return children of this node
+     */
+    @JsonIgnore
+    public Stream<SObjectNode> getChildNodes() {
+        return records.values().stream().flatMap(List::stream);
+    }
+
+    /**
+     * Returns all children of this node (one level deep) of certain type (in plural form).
+     *
+     * @param type
+     *            type of child requested in plural form (e.g for `Account` is `Accounts`)
+     * @return children of this node of specified type
+     */
+    public Stream<SObjectNode> getChildNodesOfType(final String type) {
+        ObjectHelper.notNull(type, "type");
+
+        return records.getOrDefault(type, Collections.emptyList()).stream();
+    }
+
+    /**
+     * Returns child SObjects of this node (one level deep).
+     *
+     * @return child SObjects of this node
+     */
+    @JsonIgnore
+    public Stream<AbstractSObjectBase> getChildren() {
+        return records.values().stream().flatMap(List::stream).map(SObjectNode::getObject);
+    }
+
+    /**
+     * Returns child SObjects of this node (one level deep) of certain type (in plural form)
+     *
+     * @param type
+     *            type of child requested in plural form (e.g for `Account` is `Accounts`)
+     * @return child SObjects of this node
+     */
+    public Stream<AbstractSObjectBase> getChildrenOfType(final String type) {
+        ObjectHelper.notNull(type, "type");
+
+        return records.getOrDefault(type, Collections.emptyList()).stream().map(SObjectNode::getObject);
+    }
+
+    /**
+     * Errors reported against this this node received in response to the SObject tree being submitted.
+     *
+     * @return errors for this node
+     */
+    @JsonIgnore
+    public List<RestError> getErrors() {
+        return Optional.ofNullable(errors).orElse(Collections.emptyList());
+    }
+
+    /**
+     * SObject at this node.
+     *
+     * @return SObject
+     */
+    @JsonIgnore
+    public AbstractSObjectBase getObject() {
+        return object;
+    }
+
+    /**
+     * Are there any errors resulted from the submission on this node?
+     *
+     * @return true if there are errors
+     */
+    public boolean hasErrors() {
+        return errors != null && !errors.isEmpty();
+    }
+
+    /**
+     * Size of the branch beginning with this node (number of SObjects in it).
+     *
+     * @return number of objects within this branch
+     */
+    public int size() {
+        return 1 + records.values().stream().flatMapToInt(r -> r.stream().mapToInt(SObjectNode::size)).sum();
+    }
+
+    @Override
+    public String toString() {
+        return "Node<" + getObjectType() + ">";
+    }
+
+    SObjectNode addChild(final String labelPlural, final SObjectNode node) {
+        List<SObjectNode> children = records.get(labelPlural);
+        if (children == null) {
+            children = new ArrayList<>();
+            records.put(labelPlural, children);
+        }
+
+        children.add(node);
+
+        return node;
+    }
+
+    @JsonAnyGetter
+    Map<String, Map<String, List<SObjectNode>>> children() {
+        return records.entrySet().stream()
+            .collect(Collectors.toMap(Map.Entry::getKey, e -> Collections.singletonMap("records", e.getValue())));
+    }
+
+    Attributes getAttributes() {
+        return attributes;
+    }
+
+    @JsonIgnore
+    String getObjectType() {
+        return attributes.type;
+    }
+
+    Stream<Class> objectTypes() {
+        return Stream.concat(Stream.of((Class) object.getClass()), getChildNodes().flatMap(SObjectNode::objectTypes));
+    }
+
+    void setErrors(final List<RestError> errors) {
+        this.errors = errors;
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectNodeXStreamConverter.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectNodeXStreamConverter.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectNodeXStreamConverter.java
new file mode 100644
index 0000000..68e2790
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectNodeXStreamConverter.java
@@ -0,0 +1,59 @@
+/**
+ * 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.util.List;
+import java.util.Map;
+
+import com.thoughtworks.xstream.converters.ConversionException;
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+
+public final class SObjectNodeXStreamConverter implements Converter {
+
+    @Override
+    public boolean canConvert(final Class type) {
+        return SObjectNode.class.equals(type);
+    }
+
+    @Override
+    public void marshal(final Object source, final HierarchicalStreamWriter writer, final MarshallingContext context) {
+        final SObjectNode node = (SObjectNode) source;
+
+        writer.addAttribute("type", node.attributes.type);
+        writer.addAttribute("referenceId", node.attributes.referenceId);
+
+        context.convertAnother(node.object);
+
+        for (final Map.Entry<String, List<SObjectNode>> e : node.records.entrySet()) {
+            writer.startNode(e.getKey());
+
+            context.convertAnother(e.getValue());
+
+            writer.endNode();
+        }
+    }
+
+    @Override
+    public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) {
+        throw new ConversionException("Unmarshalling is not supported");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectTree.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectTree.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectTree.java
new file mode 100644
index 0000000..8bb694c
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectTree.java
@@ -0,0 +1,294 @@
+/**
+ * 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.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import static java.util.Objects.requireNonNull;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
+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.dto.RestError;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * Payload and response for the SObject tree Composite API. The main interface for specifying what to include in the
+ * sumission to the API endpoint. To build the tree out use: <blockquote>
+ *
+ * <pre>
+ * {@code
+ * Account account = ...
+ * Contact president = ...
+ * Contact marketing = ...
+ *
+ * Account anotherAccount = ...
+ * Contact sales = ...
+ * Asset someAsset = ...
+ *
+ * SObjectTree request = new SObjectTree();
+ * request.addObject(account).addChildren(president, marketing);
+ * request.addObject(anotherAccount).addChild(sales).addChild(someAsset);
+ * }
+ * </pre>
+ *
+ * </blockquote>
+ *
+ * This will generate a tree of SObjects resembling: <blockquote>
+ *
+ * <pre>
+ * .
+ * |-- account
+ * |   |-- president
+ * |   `-- marketing
+ * `-- anotherAccount
+ *     `-- sales
+ *         `-- someAsset
+ * </pre>
+ *
+ * </blockquote>
+ *
+ * By default references that correlate between SObjects in the tree and returned identifiers and errors are handled
+ * automatically, if you wish to customize the generation of the reference implement {@link ReferenceGenerator} and
+ * supply it as constructor argument to {@link #SObjectTree(ReferenceGenerator)}.
+ * <p/>
+ * Note that the tree can hold single object type at the root of the tree.
+ *
+ * @see ReferenceGenerator
+ * @see SObjectNode
+ * @see AbstractSObjectBase
+ * @see AbstractDescribedSObjectBase
+ */
+@XStreamAlias("SObjectTreeRequest")
+public final class SObjectTree implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @XStreamImplicit
+    @JsonProperty
+    final List<SObjectNode> records = new CopyOnWriteArrayList<>();
+
+    @XStreamOmitField
+    final ReferenceGenerator referenceGenerator;
+
+    @XStreamOmitField
+    private String objectType;
+
+    /**
+     * Create new SObject tree with the default {@link ReferenceGenerator}.
+     */
+    public SObjectTree() {
+        this(new Counter());
+    }
+
+    /**
+     * Create new SObject tree with custom {@link ReferenceGenerator}.
+     */
+    public SObjectTree(final ReferenceGenerator referenceGenerator) {
+        this.referenceGenerator = requireNonNull(referenceGenerator,
+            "You must specify the ReferenceGenerator implementation");
+    }
+
+    /**
+     * Add SObject at the root of the tree.
+     *
+     * @param object
+     *            SObject to add
+     * @return {@link SObjectNode} for the given SObject
+     */
+    public SObjectNode addObject(final AbstractSObjectBase object) {
+        ObjectHelper.notNull(object, "object");
+
+        return addNode(new SObjectNode(this, object));
+    }
+
+    /**
+     * Returns a stream of all nodes in the tree.
+     *
+     * @return
+     */
+    public Stream<SObjectNode> allNodes() {
+        return records.stream().flatMap(r -> Stream.concat(Stream.of(r), r.getChildNodes()));
+    }
+
+    /**
+     * Returns a stream of all objects in the tree.
+     *
+     * @return
+     */
+    public Stream<AbstractSObjectBase> allObjects() {
+        return records.stream().flatMap(r -> Stream.concat(Stream.of(r.getObject()), r.getChildren()));
+    }
+
+    /**
+     * Returns the type of the objects in the root of the tree.
+     *
+     * @return object type
+     */
+    @JsonIgnore
+    public String getObjectType() {
+        return objectType;
+    }
+
+    public Class[] objectTypes() {
+        final Set<Class> types = records.stream().flatMap(n -> n.objectTypes()).collect(Collectors.toSet());
+
+        return types.toArray(new Class[types.size()]);
+    }
+
+    /**
+     * Sets errors for the given reference. Used when processing the response of API invocation.
+     *
+     * @param referenceId
+     *            reference identifier
+     * @param errors
+     *            list of {@link RestError}
+     */
+    public void setErrorFor(final String referenceId, final List<RestError> errors) {
+        for (final SObjectNode node : records) {
+            if (setErrorFor(node, referenceId, errors)) {
+                break;
+            }
+        }
+    }
+
+    /**
+     * Sets identifier of SObject for the given reference. Used when processing the response of API invocation.
+     *
+     * @param referenceId
+     *            reference identifier
+     * @param id
+     *            SObject identifier
+     */
+    public void setIdFor(final String referenceId, final String id) {
+        for (final SObjectNode node : records) {
+            if (setIdFor(node, referenceId, id)) {
+                break;
+            }
+        }
+    }
+
+    /**
+     * Returns the number of elements in the tree.
+     *
+     * @return number of elements in the tree
+     */
+    public int size() {
+        return records.stream().mapToInt(r -> r.size()).sum();
+    }
+
+    SObjectNode addNode(final SObjectNode node) {
+        final String givenObjectType = node.getObjectType();
+
+        if (objectType != null && !objectType.equals(givenObjectType)) {
+            throw new IllegalArgumentException("SObjectTree can hold only records of the same type, previously given: "
+                + objectType + ", and now trying to add: " + givenObjectType);
+        }
+        objectType = givenObjectType;
+
+        records.add(node);
+
+        return node;
+    }
+
+    boolean setErrorFor(final SObjectNode node, final String referenceId, final List<RestError> errors) {
+        final Attributes attributes = node.getAttributes();
+
+        final String attributesReferenceId = attributes.getReferenceId();
+
+        if (Objects.equals(attributesReferenceId, referenceId)) {
+            node.setErrors(errors);
+            return true;
+        }
+
+        return StreamSupport.stream(node.getChildNodes().spliterator(), false)
+            .anyMatch(n -> setErrorFor(n, referenceId, errors));
+    }
+
+    boolean setIdFor(final SObjectNode node, final String referenceId, final String id) {
+        final Attributes attributes = node.getAttributes();
+
+        final String attributesReferenceId = attributes.getReferenceId();
+
+        if (Objects.equals(attributesReferenceId, referenceId)) {
+            final Object object = node.getObject();
+
+            if (object instanceof AbstractSObjectBase) {
+                return updateBaseObjectId(id, object);
+            } else {
+                return updateGeneralObjectId(id, object);
+            }
+        }
+
+        return StreamSupport.stream(node.getChildNodes().spliterator(), false)
+            .anyMatch(n -> setIdFor(n, referenceId, id));
+    }
+
+    boolean updateBaseObjectId(final String id, final Object object) {
+        ((AbstractSObjectBase) object).setId(id);
+
+        return true;
+    }
+
+    boolean updateGeneralObjectId(final String id, final Object object) {
+        final Class<? extends Object> clazz = object.getClass();
+        final BeanInfo beanInfo;
+        try {
+            beanInfo = Introspector.getBeanInfo(clazz);
+        } catch (final IntrospectionException e) {
+            throw new IllegalStateException(e);
+        }
+
+        final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
+
+        final Optional<PropertyDescriptor> maybeIdProperty = Arrays.stream(propertyDescriptors)
+            .filter(pd -> "id".equals(pd.getName())).findFirst();
+
+        if (maybeIdProperty.isPresent()) {
+            final Method readMethod = maybeIdProperty.get().getReadMethod();
+            try {
+                readMethod.invoke(object, id);
+
+                return true;
+            } catch (IllegalAccessException | InvocationTargetException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectTreeResponse.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..736d917
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectTreeResponse.java
@@ -0,0 +1,64 @@
+/**
+ * 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.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
+
+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
+public final class SObjectTreeResponse implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private final boolean hasErrors;
+
+    @XStreamImplicit
+    private final List<ReferenceId> results;
+
+    @JsonCreator
+    public SObjectTreeResponse(@JsonProperty("hasErrors") final boolean hasErrors,
+            @JsonProperty("results") final List<ReferenceId> results) {
+        this.hasErrors = hasErrors;
+        this.results = Optional.ofNullable(results).orElse(Collections.emptyList());
+    }
+
+    public List<RestError> getAllErrors() {
+        return results.stream().flatMap(r -> r.getErrors().stream()).collect(Collectors.toList());
+    }
+
+    public List<ReferenceId> getResults() {
+        return results;
+    }
+
+    public boolean hasErrors() {
+        return hasErrors;
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/OperationName.java
----------------------------------------------------------------------
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 b1ffeda..d6b0cd0 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
@@ -66,7 +66,10 @@ public enum OperationName {
 
     // Approval Processes and Process Rules API
     APPROVAL("approval"),
-    APPROVALS("approvals");
+    APPROVALS("approvals"),
+
+    // Composite API
+    COMPOSITE_TREE("composite-tree");
 
     private final String value;
 

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/AbstractClientBase.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/AbstractClientBase.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/AbstractClientBase.java
index 757293e..10799f6 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/AbstractClientBase.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/AbstractClientBase.java
@@ -173,7 +173,9 @@ public abstract class AbstractClientBase implements SalesforceSession.Salesforce
                         final String msg = String.format("Error {%s:%s} executing {%s:%s}",
                             status, response.getReason(), request.getMethod(), request.getURI());
                         final SalesforceException cause = createRestException(response, getContentAsInputStream());
-                        callback.onResponse(null, new SalesforceException(msg, response.getStatus(), cause));
+
+                        // for APIs that return body on status 400, such as Composite API we need content as well
+                        callback.onResponse(getContentAsInputStream(), new SalesforceException(msg, response.getStatus(), cause));
 
                     } else {
 

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/CompositeApiClient.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..0650419
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/CompositeApiClient.java
@@ -0,0 +1,51 @@
+/**
+ * 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.internal.client;
+
+import java.util.Optional;
+
+import org.apache.camel.component.salesforce.api.SalesforceException;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectTree;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectTreeResponse;
+
+public interface CompositeApiClient {
+
+    @FunctionalInterface
+    interface Operation<T, R> {
+
+        void submit(T body, ResponseCallback<R> callback) throws SalesforceException;
+
+    }
+
+    @FunctionalInterface
+    public interface ResponseCallback<T> {
+        void onResponse(Optional<T> body, SalesforceException exception);
+    }
+
+    /**
+     * Submits given nodes (records) of SObjects and their children as a tree in a single request. And updates the
+     * <code>Id</code> parameter of each object to the value returned from the API call.
+     *
+     * @param tree
+     *            SObject tree to submit
+     * @param callback
+     *            {@link ResponseCallback} to handle response or exception
+     */
+    void submitCompositeTree(SObjectTree tree, ResponseCallback<SObjectTreeResponse> callback)
+            throws SalesforceException;
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/DefaultCompositeApiClient.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..dd92936
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/DefaultCompositeApiClient.java
@@ -0,0 +1,230 @@
+/**
+ * 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.internal.client;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.XStreamException;
+import com.thoughtworks.xstream.core.TreeMarshallingStrategy;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.io.naming.NoNameCoder;
+import com.thoughtworks.xstream.io.xml.CompactWriter;
+import com.thoughtworks.xstream.io.xml.XppDriver;
+
+import org.apache.camel.component.salesforce.SalesforceEndpointConfig;
+import org.apache.camel.component.salesforce.SalesforceHttpClient;
+import org.apache.camel.component.salesforce.api.SalesforceException;
+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;
+import org.apache.camel.component.salesforce.api.utils.JsonUtils;
+import org.apache.camel.component.salesforce.internal.PayloadFormat;
+import org.apache.camel.component.salesforce.internal.SalesforceSession;
+import org.apache.camel.util.ObjectHelper;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.util.InputStreamContentProvider;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.util.StringUtil;
+
+public class DefaultCompositeApiClient extends AbstractClientBase implements CompositeApiClient {
+
+    private final PayloadFormat format;
+
+    private ObjectMapper mapper;
+
+    private final Map<Class<?>, ObjectReader> readers = new HashMap<>();
+
+    private final Map<Class<?>, ObjectWriter> writters = new HashMap<>();
+
+    private final XStream xStream;
+
+    public DefaultCompositeApiClient(final SalesforceEndpointConfig configuration, final PayloadFormat format,
+            final String version, final SalesforceSession session, final SalesforceHttpClient httpClient)
+            throws SalesforceException {
+        super(version, session, httpClient);
+        this.format = format;
+
+        if (configuration.getObjectMapper() != null) {
+            mapper = configuration.getObjectMapper();
+        } else {
+            mapper = JsonUtils.createObjectMapper();
+        }
+
+        xStream = configureXStream();
+    }
+
+    static XStream configureXStream() {
+        final XStream xStream = new XStream(new XppDriver(new NoNameCoder()) {
+            @Override
+            public HierarchicalStreamWriter createWriter(final Writer out) {
+                return new CompactWriter(out, getNameCoder());
+            }
+
+        });
+        xStream.ignoreUnknownElements();
+        XStreamUtils.addDefaultPermissions(xStream);
+        xStream.registerConverter(new DateTimeConverter());
+        xStream.setMarshallingStrategy(new TreeMarshallingStrategy());
+        xStream.processAnnotations(new Class[] {SObjectTree.class, SObjectTreeResponse.class});
+
+        return xStream;
+    }
+
+    @Override
+    public void submitCompositeTree(final SObjectTree tree, final ResponseCallback<SObjectTreeResponse> callback)
+            throws SalesforceException {
+        final String url = versionUrl() + "composite/tree/" + tree.getObjectType();
+
+        final Request post = createRequest(HttpMethod.POST, url);
+
+        final InputStream stream = serialize(tree, tree.objectTypes());
+
+        // input stream as entity content is needed for authentication retries
+        final InputStreamContentProvider content = new InputStreamContentProvider(stream);
+        post.content(content);
+
+        doHttpRequest(post, (response, exception) -> callback
+            .onResponse(tryToReadResponse(SObjectTreeResponse.class, response), exception));
+    }
+
+    Request createRequest(final HttpMethod method, final String url) {
+        final Request request = getRequest(method, url);
+
+        // setup authorization
+        setAccessToken(request);
+
+        if (format == PayloadFormat.JSON) {
+            request.header(HttpHeader.CONTENT_TYPE, APPLICATION_JSON_UTF8);
+            request.header(HttpHeader.ACCEPT, APPLICATION_JSON_UTF8);
+        } else {
+            // must be XML
+            request.header(HttpHeader.CONTENT_TYPE, APPLICATION_XML_UTF8);
+            request.header(HttpHeader.ACCEPT, APPLICATION_XML_UTF8);
+        }
+
+        request.header(HttpHeader.ACCEPT_CHARSET, StringUtil.__UTF8);
+
+        return request;
+    }
+
+    <T> T fromJson(final Class<T> expectedType, final InputStream responseStream) throws IOException {
+        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));
+    }
+
+    ObjectWriter jsonWriterFor(final Object obj) {
+        final Class<?> type = obj.getClass();
+
+        return Optional.ofNullable(writters.get(type)).orElseGet(() -> mapper.writerFor(type));
+    }
+
+    InputStream serialize(final Object body, final Class<?>... additionalTypes) throws SalesforceException {
+
+        if (format == PayloadFormat.JSON) {
+            return toJson(body);
+        } else {
+            // must be XML
+            return toXml(body, additionalTypes);
+        }
+    }
+
+    String servicesDataUrl() {
+        return instanceUrl + "/services/data/";
+    }
+
+    InputStream toJson(final Object obj) throws SalesforceException {
+        byte[] jsonBytes;
+        try {
+            jsonBytes = jsonWriterFor(obj).writeValueAsBytes(obj);
+        } catch (final JsonProcessingException e) {
+            throw new SalesforceException("Unable to serialize given SObjectTree to JSON", e);
+        }
+
+        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) {
+        try {
+            if (format == PayloadFormat.JSON) {
+                return Optional.of(fromJson(expectedType, responseStream));
+            } else {
+                // must be XML
+                return Optional.of(fromXml(responseStream));
+            }
+        } catch (XStreamException | IOException e) {
+            return Optional.empty();
+        } finally {
+            try {
+                responseStream.close();
+            } catch (final IOException ignored) {
+            }
+        }
+    }
+
+    String versionUrl() {
+        ObjectHelper.notNull(version, "version");
+
+        return servicesDataUrl() + "v" + version + "/";
+    }
+
+    @Override
+    protected SalesforceException createRestException(final Response response, final InputStream responseContent) {
+        final String reason = response.getReason();
+        final int status = response.getStatus();
+
+        return new SalesforceException("Unexpected error: " + reason, status);
+    }
+
+    @Override
+    protected void setAccessToken(final Request request) {
+        request.getHeaders().put("Authorization", "Bearer " + accessToken);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/CompositeApiProcessor.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..87f7f95
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/CompositeApiProcessor.java
@@ -0,0 +1,160 @@
+/**
+ * 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.internal.processor;
+
+import java.util.EnumSet;
+import java.util.Optional;
+
+import org.apache.camel.AsyncCallback;
+import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.Message;
+import org.apache.camel.component.salesforce.SalesforceEndpoint;
+import org.apache.camel.component.salesforce.SalesforceEndpointConfig;
+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.SObjectTree;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectTreeResponse;
+import org.apache.camel.component.salesforce.internal.PayloadFormat;
+import org.apache.camel.component.salesforce.internal.client.CompositeApiClient;
+import org.apache.camel.component.salesforce.internal.client.DefaultCompositeApiClient;
+import org.apache.camel.util.ServiceHelper;
+
+public final class CompositeApiProcessor extends AbstractSalesforceProcessor {
+
+    @FunctionalInterface
+    interface ResponseHandler<T> {
+
+        void handleResponse(Exchange exchange, Optional<T> body, SalesforceException exception, AsyncCallback callback);
+
+    }
+
+    private final CompositeApiClient compositeClient;
+
+    private final PayloadFormat format;
+
+    public CompositeApiProcessor(final SalesforceEndpoint endpoint) throws SalesforceException {
+        super(endpoint);
+
+        final SalesforceEndpointConfig configuration = endpoint.getConfiguration();
+        final String apiVersion = configuration.getApiVersion();
+
+        format = configuration.getFormat();
+
+        if (!EnumSet.of(PayloadFormat.JSON, PayloadFormat.XML).contains(format)) {
+            throw new SalesforceException("Unsupported format: " + format, 0);
+        }
+
+        compositeClient = new DefaultCompositeApiClient(configuration, format, apiVersion, session, httpClient);
+
+    }
+
+    @Override
+    public boolean process(final Exchange exchange, final AsyncCallback callback) {
+        try {
+            switch (operationName) {
+            case COMPOSITE_TREE:
+                return processInternal(SObjectTree.class, exchange, compositeClient::submitCompositeTree,
+                    this::processCompositeTreeResponse, callback);
+            default:
+                throw new SalesforceException("Unknown operation name: " + operationName.value(), null);
+            }
+        } catch (final SalesforceException e) {
+            return processException(exchange, callback, e);
+        } catch (final RuntimeException e) {
+            final SalesforceException exception = new SalesforceException(
+                String.format("Unexpected Error processing %s: \"%s\"", operationName.value(), e.getMessage()), e);
+            return processException(exchange, callback, exception);
+        }
+    }
+
+    @Override
+    public void start() throws Exception {
+        ServiceHelper.startService(compositeClient);
+    }
+
+    @Override
+    public void stop() throws Exception {
+        ServiceHelper.stopService(compositeClient);
+    }
+
+    void processCompositeTreeResponse(final Exchange exchange, final Optional<SObjectTreeResponse> responseBody,
+        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 SObjectTree tree = in.getBody(SObjectTree.class);
+
+                final SObjectTreeResponse response = responseBody.get();
+
+                final boolean hasErrors = response.hasErrors();
+
+                for (final ReferenceId referenceId : response.getResults()) {
+                    tree.setIdFor(referenceId.getReferenceId(), referenceId.getId());
+
+                    if (hasErrors) {
+                        tree.setErrorFor(referenceId.getReferenceId(), referenceId.getErrors());
+                    }
+                }
+
+                if (hasErrors) {
+                    final SalesforceException withErrors = new SalesforceException(response.getAllErrors(),
+                        exception.getStatusCode(), exception);
+                    exchange.setException(withErrors);
+                }
+
+                out.copyFromWithNewBody(in, tree);
+            }
+        } finally {
+            // notify callback that exchange is done
+            callback.done(false);
+        }
+    }
+
+    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 {
+
+        final T body;
+
+        final Message in = exchange.getIn();
+        try {
+            body = in.getMandatoryBody(bodyType);
+        } catch (final InvalidPayloadException e) {
+            throw new SalesforceException(e);
+        }
+
+        clientOperation.submit(body,
+            (response, exception) -> responseHandler.handleResponse(exchange, response, exception, callback));
+
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiTreeIntegrationTest.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiTreeIntegrationTest.java b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiTreeIntegrationTest.java
new file mode 100644
index 0000000..cf6fb84
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiTreeIntegrationTest.java
@@ -0,0 +1,117 @@
+/**
+ * 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 org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectTree;
+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.Asset;
+import org.apache.camel.component.salesforce.dto.generated.Contact;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class CompositeApiTreeIntegrationTest extends AbstractSalesforceTestBase {
+
+    private final String format;
+
+    public CompositeApiTreeIntegrationTest(final String format) {
+        this.format = format;
+    }
+
+    @Parameters(name = "format = {0}")
+    public static Iterable<String> formats() {
+        return Arrays.asList("JSON", "XML");
+    }
+
+    @Test
+    public void shouldSubmitTreeUsingCompositeApi() {
+        final Account simpleAccount = new Account();
+
+        final Contact smith = new Contact();
+
+        final Contact evans = new Contact();
+
+        final Account simpleAccount2 = new Account();
+
+        simpleAccount.setName("SampleAccount");
+        simpleAccount.setPhone("1234567890");
+        simpleAccount.setWebsite("www.salesforce.com");
+        simpleAccount.setNumberOfEmployees(100);
+        simpleAccount.setIndustry(Account_IndustryEnum.BANKING);
+
+        smith.setLastName("Smith");
+        smith.setTitle("President");
+        smith.setEmail("sample@salesforce.com");
+
+        evans.setLastName("Evans");
+        evans.setTitle("Vice President");
+        evans.setEmail("sample@salesforce.com");
+
+        simpleAccount2.setName("SampleAccount2");
+        simpleAccount2.setPhone("1234567890");
+        simpleAccount2.setWebsite("www.salesforce2.com");
+        simpleAccount2.setNumberOfEmployees(100);
+        simpleAccount2.setIndustry(Account_IndustryEnum.BANKING);
+
+        final SObjectTree tree = new SObjectTree();
+        tree.addObject(simpleAccount).addChildren("Contacts", smith, evans);
+        tree.addObject(simpleAccount2);
+
+        final Account simpleAccount3 = new Account();
+        simpleAccount3.setName("SimpleAccount3");
+
+        final Contact contact = new Contact();
+        contact.setFirstName("Simple");
+        contact.setLastName("Contact");
+
+        final Asset asset = new Asset();
+        asset.setName("Asset Name");
+        asset.setDescription("Simple asset");
+
+        tree.addObject(simpleAccount3).addChild("Contacts", contact).addChild("Assets", asset);
+
+        final SObjectTree response = template.requestBody("salesforce:composite-tree?format=" + format, tree,
+            SObjectTree.class);
+
+        assertNotNull("Response should be provided", response);
+
+        assertNotNull("First account should have Id set", simpleAccount.getId());
+        assertNotNull("President of the first account should have Id set", smith.getId());
+        assertNotNull("Vice president of the first account should have Id set", evans.getId());
+
+        assertNotNull("Second account should have Id set", simpleAccount2.getId());
+
+        assertNotNull("Third account should have Id set", simpleAccount3.getId());
+        assertNotNull("Simple contact on third account should have Id set", contact.getId());
+        assertNotNull("Simple asset on the contact of the third account should have Id set", asset.getId());
+    }
+
+    @Override
+    protected RouteBuilder doCreateRouteBuilder() throws Exception {
+        return new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/SalesforceComponentConfigurationIntegrationTest.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/SalesforceComponentConfigurationIntegrationTest.java b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/SalesforceComponentConfigurationIntegrationTest.java
index 92c353d..d996325 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/SalesforceComponentConfigurationIntegrationTest.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/SalesforceComponentConfigurationIntegrationTest.java
@@ -27,6 +27,8 @@ import org.apache.camel.Component;
 import org.apache.camel.ComponentConfiguration;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.salesforce.dto.generated.Account;
+import org.apache.camel.component.salesforce.dto.generated.Asset;
+import org.apache.camel.component.salesforce.dto.generated.Contact;
 import org.apache.camel.component.salesforce.dto.generated.Document;
 import org.apache.camel.component.salesforce.dto.generated.Line_Item__c;
 import org.apache.camel.component.salesforce.dto.generated.MSPTest;
@@ -106,7 +108,7 @@ public class SalesforceComponentConfigurationIntegrationTest extends CamelTestSu
             "query", "queryMore", "queryAll", "search", "apexCall", "recent", "createJob", "getJob", "closeJob", "abortJob",
             "createBatch", "getBatch", "getAllBatches", "getRequest", "getResults", "createBatchQuery", "getQueryResultIds",
             "getQueryResult", "getRecentReports", "getReportDescription", "executeSyncReport", "executeAsyncReport",
-            "getReportInstances", "getReportResults", "limits", "approval", "approvals", "[PushTopicName]"
+            "getReportInstances", "getReportResults", "limits", "approval", "approvals", "composite-tree", "[PushTopicName]"
         );
 
         // get filtered operation names
@@ -153,7 +155,7 @@ public class SalesforceComponentConfigurationIntegrationTest extends CamelTestSu
 
         // get sObjectName values, from scanned DTO packages
         assertCompletionOptions(configuration.completeEndpointPath("getSObject?sObjectName="),
-            "Account", "Tasks__c", "Line_Item__c", "Merchandise__c", "Document", "MSPTest");
+            "Account", "Asset", "Contact", "Tasks__c", "Line_Item__c", "Merchandise__c", "Document", "MSPTest");
 
         // get sObjectFields values, from scanned DTO
         assertCompletionOptions(
@@ -165,6 +167,8 @@ public class SalesforceComponentConfigurationIntegrationTest extends CamelTestSu
         // get sObjectClass values, from scanned DTO packages
         assertCompletionOptions(configuration.completeEndpointPath("getSObject?sObjectClass="),
             Account.class.getName(),
+            Asset.class.getName(),
+            Contact.class.getName(),
             Tasks__c.class.getName(),
             Line_Item__c.class.getName(),
             Merchandise__c.class.getName(),

http://git-wip-us.apache.org/repos/asf/camel/blob/e1cfeb5a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/RestErrorTest.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/RestErrorTest.java b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/RestErrorTest.java
new file mode 100644
index 0000000..a2fc0c4
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/RestErrorTest.java
@@ -0,0 +1,61 @@
+/**
+ * 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;
+
+import java.util.Arrays;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.thoughtworks.xstream.XStream;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class RestErrorTest {
+
+    RestError error = new RestError("errorCode", "message", Arrays.asList("field1", "field2"));
+
+    @Test
+    public void shouldDeserializeFromJson() throws Exception {
+        final ObjectMapper objectMapper = new ObjectMapper();
+        final ObjectReader reader = objectMapper.readerFor(RestError.class);
+
+        final RestError gotWithErrorCode = reader.<RestError> readValue(
+            "{\"errorCode\":\"errorCode\",\"message\":\"message\",\"fields\":[ \"field1\",\"field2\" ]}");
+        assertEquals(gotWithErrorCode, error);
+
+        final RestError gotWithStatusCode = reader.<RestError> readValue(
+            "{\"statusCode\":\"errorCode\",\"message\":\"message\",\"fields\":[ \"field1\",\"field2\" ]}");
+        assertEquals(gotWithStatusCode, error);
+    }
+
+    @Test
+    public void shouldDeserializeFromXml() {
+        final XStream xStream = new XStream();
+        xStream.processAnnotations(RestError.class);
+        xStream.alias("errors", RestError.class);
+
+        final RestError gotWithErrorCode = (RestError) xStream.fromXML(
+            "<errors><fields>field1</fields><fields>field2</fields><message>message</message><errorCode>errorCode</errorCode></errors>");
+        assertEquals(gotWithErrorCode, error);
+
+        final RestError gotWithStatusCode = (RestError) xStream.fromXML(
+            "<errors><fields>field1</fields><fields>field2</fields><message>message</message><statusCode>errorCode</statusCode></errors>");
+        assertEquals(gotWithStatusCode, error);
+    }
+}