camel-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From davscl...@apache.org
Subject [2/2] camel git commit: CAMEL-10593 Support for Composite API batch
Date Wed, 14 Dec 2016 12:10:44 GMT
CAMEL-10593 Support for Composite API batch

This commit implements support for Salesforce Composite Batch API[1]
that allows the user to combine up to 25 requests in a single batch and
then send them in a single HTTP request saving on the request round trip
time and bandwidth.

One would use this operation like this:

//Create the batch request:
final SObjectBatch batch = new SObjectBatch("38.0");

final Account updates = new Account();
updates.set...

//Use the builder methods to add up to 25 operations
batch.addUpdate("Account", accountId, updates)
     .addGet("Account", "001D000000K0fXOIAZ")
     .add...

final SObjectBatchResponse response =
template.requestBody("salesforce:composite-batch?format=JSON", batch,
SObjectBatchResponse.class);

[1]
https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_batch.htm


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

Branch: refs/heads/master
Commit: 65b7ac7272ba6a94cdce2b57c7c0f80cc4a51d5f
Parents: 8718152
Author: Zoran Regvart <zoran@regvart.com>
Authored: Fri Dec 9 12:13:46 2016 +0100
Committer: Claus Ibsen <davsclaus@apache.org>
Committed: Wed Dec 14 13:09:13 2016 +0100

----------------------------------------------------------------------
 .../src/main/docs/salesforce-component.adoc     |  60 +++
 .../salesforce/SalesforceProducer.java          |   1 +
 .../api/dto/AnnotationFieldKeySorter.java       |  69 +++
 .../salesforce/api/dto/XStreamFieldOrder.java   |  34 ++
 .../api/dto/composite/BatchRequest.java         |  72 ++++
 .../api/dto/composite/MapOfMapsConverter.java   |  91 ++++
 .../api/dto/composite/RichInputConverter.java   |  67 +++
 .../api/dto/composite/SObjectBatch.java         | 424 +++++++++++++++++++
 .../api/dto/composite/SObjectBatchResponse.java |  58 +++
 .../api/dto/composite/SObjectBatchResult.java   | 123 ++++++
 .../component/salesforce/api/utils/Version.java |  99 +++++
 .../salesforce/internal/OperationName.java      |   3 +-
 .../internal/client/CompositeApiClient.java     |   5 +
 .../client/DefaultCompositeApiClient.java       |  58 ++-
 .../processor/CompositeApiProcessor.java        |  24 ++
 .../salesforce/AbstractSalesforceTestBase.java  |   6 +-
 .../CompositeApiBatchIntegrationTest.java       | 338 +++++++++++++++
 ...ceComponentConfigurationIntegrationTest.java |   3 +-
 .../dto/composite/MapOfMapsConverterTest.java   | 154 +++++++
 .../dto/composite/SObjectBatchResponseTest.java | 156 +++++++
 .../api/dto/composite/SObjectBatchTest.java     | 229 ++++++++++
 .../salesforce/api/utils/VersionTest.java       |  76 ++++
 22 files changed, 2136 insertions(+), 14 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/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 e36dcbb..cbbf0b1 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
@@ -72,6 +72,7 @@ results) using result link returned from the 'query' API
 * 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
+* composite-batch - submit a composition of requests in batch
 
 For example, the following producer endpoint uses the upsertSObject API,
 with the sObjectIdName parameter specifying 'Name' as the external id
@@ -376,6 +377,65 @@ final List<SObjectNode> succeeded = result.get(false);
 final String firstId = succeeded.get(0).getId();
 -----------------------------------------------------------------------------------------------------
 
+[[Salesforce-CompositeAPI-Batch]]
+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.
+
+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
+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. Note that the responses will vary between XML and JSON, this is due to the
+responses from Salesforce API being different. So be careful if you switch between formats without changing the response
+handling code.
+
+Lets look at an example:
+
+[source,java]
+-----------------------------------------------------------------------------------------------------
+final String acountId = ...
+final SObjectBatch batch = new SObjectBatch("38.0");
+
+final Account updates = new Account();
+updates.setName("NewName");
+batch.addUpdate("Account", accountId, updates);
+
+final Account newAccount = new Account();
+newAccount.setName("Account created from Composite batch API");
+batch.addCreate(newAccount);
+
+batch.addGet("Account", accountId, "Name", "BillingPostalCode");
+
+batch.addDelete("Account", accountId);
+
+final SObjectBatchResponse response = template.requestBody("salesforce:composite-batch?format=JSON", batch, SObjectBatchResponse.class);
+
+boolean hasErrors = response.hasErrors(); // if any of the requests has resulted in either 4xx or 5xx HTTP status
+final List<SObjectBatchResult> results = response.getResults(); // results of three operations sent in batch
+
+final SObjectBatchResult updateResult = results.get(0); // update result
+final int updateStatus = updateResult.getStatusCode(); // probably 204
+final Object updateResultData = updateResult.getResult(); // probably null
+
+final SObjectBatchResult createResult = results.get(1); // create result
+@SuppressWarnings("unchecked")
+final Map<String, Object> createData = (Map<String, Object>) createResult.getResult();
+final String newAccountId = createData.get("id"); // id of the new account, this is for JSON, for XML it would be createData.get("Result").get("id")
+
+final SObjectBatchResult retrieveResult = results.get(2); // retrieve result
+@SuppressWarnings("unchecked")
+final Map<String, Object> retrieveData = (Map<String, Object>) retrieveResult.getResult();
+final String accountName = retrieveData.get("Name"); // Name of the retrieved account, this is for JSON, for XML it would be createData.get("Account").get("Name")
+final String accountBillingPostalCode = retrieveData.get("BillingPostalCode"); // Name of the retrieved account, this is for JSON, for XML it would be createData.get("Account").get("BillingPostalCode")
+
+final SObjectBatchResult deleteResult = results.get(3); // delete result
+final int updateStatus = deleteResult.getStatusCode(); // probably 204
+final Object updateResultData = deleteResult.getResult(); // probably null
+
+-----------------------------------------------------------------------------------------------------
+
 [[Salesforce-CamelSalesforceMavenPlugin]]
 Camel Salesforce Maven Plugin
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/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 ed68813..5c9c7e3 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
@@ -99,6 +99,7 @@ public class SalesforceProducer extends DefaultAsyncProducer {
     private boolean isCompositeOperation(OperationName operationName) {
         switch (operationName) {
         case COMPOSITE_TREE:
+        case COMPOSITE_BATCH:
             return true;
         default:
             return false;

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/AnnotationFieldKeySorter.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/AnnotationFieldKeySorter.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/AnnotationFieldKeySorter.java
new file mode 100644
index 0000000..ff7ecdd
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/AnnotationFieldKeySorter.java
@@ -0,0 +1,69 @@
+/**
+ * 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.lang.reflect.Field;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import com.thoughtworks.xstream.converters.reflection.FieldKey;
+import com.thoughtworks.xstream.converters.reflection.FieldKeySorter;
+
+public final class AnnotationFieldKeySorter implements FieldKeySorter {
+
+    private static final class AnnotationFieldOrderComparator implements Comparator<FieldKey> {
+        private final SortedMap<String, Integer> order = new TreeMap<>();
+
+        private AnnotationFieldOrderComparator(final String[] orderedFields, Field[] fields) {
+            int i = 0;
+            for (; i < orderedFields.length; i++) {
+                order.put(orderedFields[i], i);
+            }
+            for (int j = 0; j < fields.length; j++) {
+                order.putIfAbsent(fields[j].getName(), i + j);
+            }
+        }
+
+        @Override
+        public int compare(final FieldKey k1, final FieldKey k2) {
+            final String field1 = k1.getFieldName();
+            final String field2 = k2.getFieldName();
+
+            return order.get(field1).compareTo(order.get(field2));
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Map sort(final Class type, final Map keyedByFieldKey) {
+        final Class<?> clazz = type;
+
+        final XStreamFieldOrder fieldOrderAnnotation = clazz.getAnnotation(XStreamFieldOrder.class);
+        if (fieldOrderAnnotation == null) {
+            return keyedByFieldKey;
+        }
+
+        final String[] fieldOrder = fieldOrderAnnotation.value();
+        final TreeMap<FieldKey, Field> sorted = new TreeMap<>(
+            new AnnotationFieldOrderComparator(fieldOrder, type.getDeclaredFields()));
+        sorted.putAll(keyedByFieldKey);
+
+        return sorted;
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/XStreamFieldOrder.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/XStreamFieldOrder.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/XStreamFieldOrder.java
new file mode 100644
index 0000000..db435e4
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/XStreamFieldOrder.java
@@ -0,0 +1,34 @@
+/**
+ * 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.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Documented
+@Retention(RUNTIME)
+@Target(TYPE)
+public @interface XStreamFieldOrder {
+
+    /** String array containing the order of the fields in serialized XML */
+    String[] value();
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/BatchRequest.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/BatchRequest.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/BatchRequest.java
new file mode 100644
index 0000000..afdea82
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/BatchRequest.java
@@ -0,0 +1,72 @@
+/**
+ * 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.SObjectBatch.Method;
+
+@XStreamAlias("batchRequest")
+@XStreamFieldOrder({"method", "url", "richInput"})
+@JsonInclude(Include.NON_NULL)
+@JsonPropertyOrder({"method", "url", "richInput"})
+final class BatchRequest implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private final Method method;
+
+    @XStreamConverter(RichInputConverter.class)
+    private final Object richInput;
+
+    private final String url;
+
+    BatchRequest(final Method method, final String url) {
+        this(method, url, null);
+    }
+
+    BatchRequest(final Method method, final String url, final Object richInput) {
+        this.method = method;
+        this.url = url;
+        this.richInput = richInput;
+    }
+
+    public Method getMethod() {
+        return method;
+    }
+
+    public Object getRichInput() {
+        return richInput;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    @Override
+    public String toString() {
+        return "Batch: " + method + " " + url + ", data:" + richInput;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/MapOfMapsConverter.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/MapOfMapsConverter.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/MapOfMapsConverter.java
new file mode 100644
index 0000000..9eeba0a
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/MapOfMapsConverter.java
@@ -0,0 +1,91 @@
+/**
+ * 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.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+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 class MapOfMapsConverter implements Converter {
+
+    @Override
+    public boolean canConvert(final Class type) {
+        return true;
+    }
+
+    @Override
+    public void marshal(final Object source, final HierarchicalStreamWriter writer, final MarshallingContext context) {
+        context.convertAnother(source);
+    }
+
+    @Override
+    public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) {
+        final Map<String, Object> ret = new HashMap<>();
+
+        while (reader.hasMoreChildren()) {
+            readMap(reader, ret);
+        }
+
+        return ret;
+    }
+
+    Object readMap(final HierarchicalStreamReader reader, final Map<String, Object> map) {
+        if (reader.hasMoreChildren()) {
+            reader.moveDown();
+            final String key = reader.getNodeName();
+
+            final Map<String, String> attributes = new HashMap<>();
+            final Iterator attributeNames = reader.getAttributeNames();
+            if (attributeNames.hasNext()) {
+                while (attributeNames.hasNext()) {
+                    final String attributeName = (String) attributeNames.next();
+                    attributes.put(attributeName, reader.getAttribute(attributeName));
+                }
+            }
+
+            Object nested = readMap(reader, new HashMap<>());
+            if (!attributes.isEmpty()) {
+                if (nested instanceof String) {
+                    HashMap<Object, Object> newNested = new HashMap<>();
+                    newNested.put(key, nested);
+                    newNested.put("attributes", attributes);
+                    nested = newNested;
+                } else {
+                    @SuppressWarnings("unchecked")
+                    final Map<String, Object> nestedMap = (Map<String, Object>) nested;
+                    nestedMap.put("attributes", attributes);
+                }
+            }
+
+            map.put(key, nested);
+            reader.moveUp();
+
+            readMap(reader, map);
+        } else {
+            return reader.getValue();
+        }
+
+        return map;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/RichInputConverter.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/RichInputConverter.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/RichInputConverter.java
new file mode 100644
index 0000000..fbe38e6
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/RichInputConverter.java
@@ -0,0 +1,67 @@
+/**
+ * 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.Map;
+
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.ConverterLookup;
+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 RichInputConverter implements Converter {
+
+    private final ConverterLookup converterLookup;
+
+    public RichInputConverter(final ConverterLookup converterLookup) {
+        this.converterLookup = converterLookup;
+    }
+
+    @Override
+    public boolean canConvert(final Class type) {
+        return true;
+    }
+
+    @Override
+    public void marshal(final Object source, final HierarchicalStreamWriter writer, final MarshallingContext context) {
+        if (source instanceof Map) {
+            @SuppressWarnings("unchecked")
+            final Map<String, String> map = (Map) source;
+
+            for (final Map.Entry<String, String> e : map.entrySet()) {
+                writer.startNode(e.getKey());
+                writer.setValue(e.getValue());
+                writer.endNode();
+            }
+        } else {
+            final Class<?> clazz = source.getClass();
+
+            writer.startNode(clazz.getSimpleName());
+            final Converter converter = converterLookup.lookupConverterForType(source.getClass());
+            converter.marshal(source, writer, context);
+            writer.endNode();
+        }
+    }
+
+    @Override
+    public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) {
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatch.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatch.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatch.java
new file mode 100644
index 0000000..950ce6f
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatch.java
@@ -0,0 +1,424 @@
+/**
+ * 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.Arrays;
+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.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 static org.apache.camel.util.ObjectHelper.notNull;
+import static org.apache.camel.util.StringHelper.notEmpty;
+
+/**
+ * Builder for Composite API batch request. Composite API is available from Salesforce API version 34.0 onwards its a
+ * way to combine multiple requests in a batch and submit them in one HTTP request. This object help to build the
+ * payload of the batch request. 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
+ * SObjectBatch batch = new SObjectBatch("37.0");
+ *
+ * final Account account = new Account();
+ * account.setName("NewAccountName");
+ * account.setIndustry(Account_IndustryEnum.ENVIRONMENTAL);
+ * batch.addCreate(account);
+ *
+ * batch.addDelete("Account", "001D000000K0fXOIAZ");
+ *
+ * batch.addGet("Account", "0010Y00000Arwt6QAB", "Name", "BillingPostalCode");
+ * }
+ *
+ * </pre>
+ *
+ * </blockquote>
+ *
+ * This will build a batch of three operations, one to create new Account, one to delete an Account, and one to get two
+ * fields from an Account.
+ */
+@XStreamAlias("batch")
+public final class SObjectBatch implements Serializable {
+
+    public enum Method {
+        DELETE, GET, PATCH, POST
+    }
+
+    private static final int MAX_BATCH = 25;
+
+    private static final long serialVersionUID = 1L;
+
+    @XStreamOmitField
+    private final String apiPrefix;
+
+    private final List<BatchRequest> batchRequests = new ArrayList<>();
+
+    @XStreamOmitField
+    private final Version version;
+
+    /**
+     * Create new batch 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 SObjectBatch(final String apiVersion) {
+        final String givenApiVersion = Objects.requireNonNull(apiVersion, "apiVersion");
+
+        version = Version.create(apiVersion);
+
+        version.requireAtLeast(34, 0);
+
+        this.apiPrefix = "v" + givenApiVersion;
+    }
+
+    static String composeFieldsParameter(final String... fields) {
+        if (fields != null && fields.length > 0) {
+            return "?fields=" + Arrays.stream(fields).collect(Collectors.joining(","));
+        } else {
+            return "";
+        }
+    }
+
+    /**
+     * Add create SObject to the batch request.
+     *
+     * @param data
+     *            object to create
+     *
+     * @return this batch builder
+     */
+    public SObjectBatch addCreate(final AbstractDescribedSObjectBase data) {
+        addBatchRequest(new BatchRequest(Method.POST, apiPrefix + "/sobjects/" + typeOf(data) + "/", data));
+
+        return this;
+    }
+
+    /**
+     * Add delete SObject with identifier to the batch request.
+     *
+     * @param type
+     *            type of SObject
+     * @param id
+     *            identifier of the object
+     * @return this batch builder
+     */
+    public SObjectBatch addDelete(final String type, final String id) {
+        addBatchRequest(new BatchRequest(Method.DELETE, rowBaseUrl(type, id)));
+
+        return this;
+    }
+
+    /**
+     * Generic way to add requests to batch. 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 SObjectBatch addGeneric(final Method method, final String url) {
+        addGeneric(method, url, null);
+
+        return this;
+    }
+
+    /**
+     * Generic way to add requests to batch 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 SObjectBatch addGeneric(final Method method, final String url, final Object richInput) {
+        addBatchRequest(new BatchRequest(method, apiPrefix + url, richInput));
+
+        return this;
+    }
+
+    /**
+     * Add field retrieval of an SObject by identifier to the batch request.
+     *
+     * @param type
+     *            type of SObject
+     * @param id
+     *            identifier of SObject
+     * @param fields
+     *            to return
+     * @return this batch builder
+     */
+    public SObjectBatch addGet(final String type, final String id, final String... fields) {
+        final String fieldsParameter = composeFieldsParameter(fields);
+
+        addBatchRequest(new BatchRequest(Method.GET, rowBaseUrl(type, id) + fieldsParameter));
+
+        return this;
+    }
+
+    /**
+     * Add field retrieval of an SObject by external identifier to the batch 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 SObjectBatch addGetByExternalId(final String type, final String fieldName, final String fieldValue) {
+        addBatchRequest(new BatchRequest(Method.GET, rowBaseUrl(type, fieldName, fieldValue)));
+
+        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 SObjectBatch addGetRelated(final String type, final String id, final String relation,
+        final String... fields) {
+        version.requireAtLeast(36, 0);
+
+        final String fieldsParameter = composeFieldsParameter(fields);
+
+        addBatchRequest(new BatchRequest(Method.GET,
+            rowBaseUrl(type, id) + "/" + notEmpty(relation, "relation") + fieldsParameter));
+
+        return this;
+    }
+
+    /**
+     * Add retrieval of limits to the batch.
+     *
+     * @return this batch builder
+     */
+    public SObjectBatch addLimits() {
+        addBatchRequest(new BatchRequest(Method.GET, apiPrefix + "/limits/"));
+
+        return this;
+    }
+
+    /**
+     * Add retrieval of SObject records by query to the batch.
+     *
+     * @param query
+     *            SOQL query to execute
+     * @return this batch builder
+     */
+    public SObjectBatch addQuery(final String query) {
+        addBatchRequest(new BatchRequest(Method.GET, apiPrefix + "/query/?q=" + notEmpty(query, "query")));
+
+        return this;
+    }
+
+    /**
+     * Add retrieval of all SObject records by query to the batch.
+     *
+     * @param query
+     *            SOQL query to execute
+     * @return this batch builder
+     */
+    public SObjectBatch addQueryAll(final String query) {
+        addBatchRequest(new BatchRequest(Method.GET, apiPrefix + "/queryAll/?q=" + notEmpty(query, "query")));
+
+        return this;
+    }
+
+    /**
+     * Add retrieval of SObject records by search to the batch.
+     *
+     * @param query
+     *            SOSL search to execute
+     * @return this batch builder
+     */
+    public SObjectBatch addSearch(final String searchString) {
+        addBatchRequest(
+            new BatchRequest(Method.GET, apiPrefix + "/search/?q=" + notEmpty(searchString, "searchString")));
+
+        return this;
+    }
+
+    /**
+     * Add update of SObject record to the batch. 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 SObjectBatch addUpdate(final String type, final String id, final AbstractSObjectBase data) {
+        addBatchRequest(new BatchRequest(Method.PATCH, rowBaseUrl(type, notEmpty(id, "data.Id")), data));
+
+        return this;
+    }
+
+    /**
+     * Add update of SObject record by external identifier to the batch. 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 SObjectBatch addUpdateByExternalId(final String type, final String fieldName, final String fieldValue,
+        final AbstractSObjectBase data) {
+
+        addBatchRequest(new BatchRequest(Method.PATCH, rowBaseUrl(type, fieldName, fieldValue), data));
+
+        return this;
+    }
+
+    /**
+     * Add insert or update of SObject record by external identifier to the batch. 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 SObjectBatch addUpsertByExternalId(final String type, final String fieldName, final String fieldValue,
+        final AbstractSObjectBase data) {
+
+        return addUpdateByExternalId(type, fieldName, fieldValue, data);
+    }
+
+    /**
+     * Fetches batch requests contained in this batch.
+     *
+     * @return all requests
+     */
+    public List<BatchRequest> getBatchRequests() {
+        return Collections.unmodifiableList(batchRequests);
+    }
+
+    /**
+     * Version of Salesforce API for this batch request.
+     *
+     * @return the version
+     */
+    @JsonIgnore
+    public Version getVersion() {
+        return version;
+    }
+
+    /**
+     * Returns all object types nested within this batch, needed for serialization.
+     *
+     * @return all object types in this batch
+     */
+    public Class[] objectTypes() {
+        final Set<Class<?>> types = Stream
+            .concat(Stream.of(SObjectBatch.class, BatchRequest.class),
+                batchRequests.stream().map(BatchRequest::getRichInput).filter(Objects::nonNull).map(Object::getClass))
+            .collect(Collectors.toSet());
+
+        return types.toArray(new Class[types.size()]);
+    }
+
+    void addBatchRequest(final BatchRequest batchRequest) {
+        if (batchRequests.size() >= MAX_BATCH) {
+            throw new IllegalArgumentException("You can add up to " + MAX_BATCH
+                + " requests in a single batch. Split your requests across multiple batches.");
+        }
+        batchRequests.add(batchRequest);
+    }
+
+    String rowBaseUrl(final String type, final String id) {
+        return apiPrefix + "/sobjects/" + notEmpty(type, "type") + "/" + notEmpty(id, "id");
+    }
+
+    String rowBaseUrl(final String type, final String fieldName, final String fieldValue) {
+        try {
+            return apiPrefix + "/sobjects/" + notEmpty(type, "type") + "/" + notEmpty(fieldName, "fieldName") + "/"
+                + URLEncoder.encode(notEmpty(fieldValue, "fieldValue"), StandardCharsets.UTF_8.name());
+        } catch (final UnsupportedEncodingException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    String typeOf(final AbstractDescribedSObjectBase data) {
+        return notNull(data, "data").description().getName();
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatchResponse.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatchResponse.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatchResponse.java
new file mode 100644
index 0000000..62e882d
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatchResponse.java
@@ -0,0 +1,58 @@
+/**
+ * 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 batch request it contains individual results of each request submitted in a batch at the same
+ * index. The flag {@link #hasErrors()} indicates if any of the requests in the batch has failed with status 400 or 500.
+ */
+@XStreamAlias("batchResults")
+public final class SObjectBatchResponse implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private final boolean hasErrors;
+
+    private final List<SObjectBatchResult> results;
+
+    @JsonCreator
+    public SObjectBatchResponse(@JsonProperty("hasErrors") final boolean hasErrors,
+            @JsonProperty("results") final List<SObjectBatchResult> results) {
+        this.hasErrors = hasErrors;
+        this.results = results;
+    }
+
+    public List<SObjectBatchResult> getResults() {
+        return results;
+    }
+
+    public boolean hasErrors() {
+        return hasErrors;
+    }
+
+    @Override
+    public String toString() {
+        return "hasErrors: " + hasErrors + ", results: " + results;
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatchResult.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatchResult.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatchResult.java
new file mode 100644
index 0000000..91c06ab
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/dto/composite/SObjectBatchResult.java
@@ -0,0 +1,123 @@
+/**
+ * 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 batch request. As batch requests can partially succeed or fail make
+ * sure you check the {@link #getStatusCode()} for status of the specific request. The result of the request can vary
+ * from API to API so here it is given as {@link Object}, in most cases it will be a {@link Map} with string keys and
+ * values or other {@link 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. Note that the
+ * responses will vary between XML and JSON, this is due to the responses from Salesforce API being different.
+ * <p>
+ * For example response for SObject record creation in JSON will be: <blockquote>
+ *
+ * <pre>
+ * {
+ *   "statusCode": 201,
+ *   "result": {
+ *     "id" : "0010Y00000Ary8hQAB",
+ *     "success" : true,
+ *     "errors" : []
+ *   }
+ * }
+ * </pre>
+ *
+ * </blockquote>
+ * <p>
+ * Which will result in {@link #getResult()} returning {@link Map} created like: <blockquote>
+ *
+ * <pre>
+ * {@code
+ * Map<String, Object> result = new HashMap<>();
+ * result.put("id", "0010Y00000Ary91QAB");
+ * result.put("success", Boolean.TRUE);
+ * result.put("errors", Collections.emptyList());
+ * }
+ * </pre>
+ *
+ * </blockquote>
+ * <p>
+ * Whereas using XML format the response will be: <blockquote>
+ *
+ * <pre>
+ * {@code
+ * <Result>
+ *   <id>0010Y00000AryACQAZ</id>
+ *   <success>true</success>
+ * </Result>
+ * }
+ * </pre>
+ *
+ * </blockquote>
+ * <p>
+ * And that results in {@link #getResult()} returning {@link Map} created like: <blockquote>
+ *
+ * <pre>
+ * {@code
+ * Map<String, Object> result = new HashMap<>();
+ *
+ * Map<String, Object> nestedResult = new HashMap<>();
+ * result.put("Result", nestedResult);
+ *
+ * nestedResult.put("id", "0010Y00000Ary91QAB");
+ * nestedResult.put("success", "true");
+ * }
+ * </pre>
+ *
+ * </blockquote>
+ * <p>
+ * Note the differences between type and nested {@link Map} one level deeper in the case of XML.
+ */
+@XStreamAlias("batchResult")
+public final class SObjectBatchResult implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @XStreamConverter(MapOfMapsConverter.class)
+    private final Object result;
+
+    private final int statusCode;
+
+    @JsonCreator
+    public SObjectBatchResult(@JsonProperty("statusCode") final int statusCode,
+            @JsonProperty("result") final Object result) {
+        this.statusCode = statusCode;
+        this.result = result;
+    }
+
+    public Object getResult() {
+        return result;
+    }
+
+    public int getStatusCode() {
+        return statusCode;
+    }
+
+    @Override
+    public String toString() {
+        return "<statusCode: " + statusCode + ", result: " + result + ">";
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/utils/Version.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/utils/Version.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/utils/Version.java
new file mode 100644
index 0000000..41d681b
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/utils/Version.java
@@ -0,0 +1,99 @@
+/**
+ * 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.utils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class Version implements Comparable<Version> {
+    private static final Pattern VERSION_PATTERN = Pattern.compile("(\\d+)\\.(\\d+)");
+
+    private final int major;
+
+    private final int minor;
+
+    private Version(final int major, final int minor) {
+        this.major = major;
+        this.minor = minor;
+    }
+
+    public static Version create(final String version) {
+        final Matcher matcher = VERSION_PATTERN.matcher(version);
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException(
+                "API version needs to be in <number>.<number> format, given: " + version);
+        }
+
+        final int major = Integer.parseInt(matcher.group(1));
+        final int minor = Integer.parseInt(matcher.group(2));
+
+        return new Version(major, minor);
+    }
+
+    @Override
+    public int compareTo(final Version other) {
+        final int majorCompare = Integer.compare(major, other.major);
+
+        if (majorCompare == 0) {
+            return Integer.compare(minor, other.minor);
+        } else {
+            return majorCompare;
+        }
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof Version)) {
+            return false;
+        }
+
+        final Version other = (Version) obj;
+
+        return compareTo(other) == 0;
+    }
+
+    public int getMajor() {
+        return major;
+    }
+
+    public int getMinor() {
+        return minor;
+    }
+
+    @Override
+    public int hashCode() {
+        return 1 + 31 * (1 + 31 * major) + minor;
+    }
+
+    @Override
+    public String toString() {
+        return "v" + major + "." + minor;
+    }
+
+    public void requireAtLeast(final int requiredMajor, final int requiredMinor) {
+        final Version required = new Version(requiredMajor, requiredMinor);
+
+        if (this.compareTo(required) < 0) {
+            throw new UnsupportedOperationException("This operation requires API version at least " + requiredMajor
+                + "." + requiredMinor + ", currently configured for " + major + "." + minor);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/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 d6b0cd0..3f18dbc 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
@@ -69,7 +69,8 @@ public enum OperationName {
     APPROVALS("approvals"),
 
     // Composite API
-    COMPOSITE_TREE("composite-tree");
+    COMPOSITE_TREE("composite-tree"),
+    COMPOSITE_BATCH("composite-batch");
 
     private final String value;
 

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/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
index 0650419..0caa1db 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
@@ -19,6 +19,8 @@ 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.SObjectBatch;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResponse;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectTree;
 import org.apache.camel.component.salesforce.api.dto.composite.SObjectTreeResponse;
 
@@ -36,6 +38,9 @@ public interface CompositeApiClient {
         void onResponse(Optional<T> body, SalesforceException exception);
     }
 
+    void submitCompositeBatch(SObjectBatch batch, ResponseCallback<SObjectBatchResponse> callback)
+            throws SalesforceException;
+
     /**
      * 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.

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/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
index dd92936..17fc7fb 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
@@ -31,6 +31,8 @@ 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.converters.reflection.FieldDictionary;
+import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
 import com.thoughtworks.xstream.core.TreeMarshallingStrategy;
 import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
 import com.thoughtworks.xstream.io.naming.NoNameCoder;
@@ -40,13 +42,18 @@ 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.AnnotationFieldKeySorter;
+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.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.api.utils.Version;
 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.ContentProvider;
 import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.client.api.Response;
 import org.eclipse.jetty.client.util.InputStreamContentProvider;
@@ -56,6 +63,9 @@ import org.eclipse.jetty.util.StringUtil;
 
 public class DefaultCompositeApiClient extends AbstractClientBase implements CompositeApiClient {
 
+    private static final Class[] ADDITIONAL_TYPES = new Class[] {SObjectTree.class, SObjectTreeResponse.class,
+        SObjectBatchResponse.class};
+
     private final PayloadFormat format;
 
     private ObjectMapper mapper;
@@ -82,33 +92,56 @@ public class DefaultCompositeApiClient extends AbstractClientBase implements Com
     }
 
     static XStream configureXStream() {
-        final XStream xStream = new XStream(new XppDriver(new NoNameCoder()) {
+        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(new Class[] {SObjectTree.class, SObjectTreeResponse.class});
+        xStream.processAnnotations(ADDITIONAL_TYPES);
 
         return xStream;
     }
 
     @Override
+    public void submitCompositeBatch(final SObjectBatch batch, final ResponseCallback<SObjectBatchResponse> callback)
+            throws SalesforceException {
+        final Version batchVersion = batch.getVersion();
+        if (Version.create(version).compareTo(batchVersion) <= 0) {
+            throw new SalesforceException("Component is configured with Salesforce API version " + version
+                + ", but the Composite API batch operation requires at least " + batchVersion, 0);
+        }
+
+        final String url = versionUrl() + "composite/batch";
+
+        final Request post = createRequest(HttpMethod.POST, url);
+
+        final ContentProvider content = serialize(batch, batch.objectTypes());
+        post.content(content);
+
+        doHttpRequest(post, (response, exception) -> callback
+            .onResponse(tryToReadResponse(SObjectBatchResponse.class, response), exception));
+    }
+
+    @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);
+        final ContentProvider content = serialize(tree, tree.objectTypes());
         post.content(content);
 
         doHttpRequest(post, (response, exception) -> callback
@@ -156,14 +189,17 @@ public class DefaultCompositeApiClient extends AbstractClientBase implements Com
         return Optional.ofNullable(writters.get(type)).orElseGet(() -> mapper.writerFor(type));
     }
 
-    InputStream serialize(final Object body, final Class<?>... additionalTypes) throws SalesforceException {
-
+    ContentProvider serialize(final Object body, final Class<?>... additionalTypes) throws SalesforceException {
+        final InputStream stream;
         if (format == PayloadFormat.JSON) {
-            return toJson(body);
+            stream = toJson(body);
         } else {
             // must be XML
-            return toXml(body, additionalTypes);
+            stream = toXml(body, additionalTypes);
         }
+
+        // input stream as entity content is needed for authentication retries
+        return new InputStreamContentProvider(stream);
     }
 
     String servicesDataUrl() {

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/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
index 87f7f95..e125c10 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
@@ -27,6 +27,8 @@ 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.SObjectBatch;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResponse;
 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;
@@ -70,6 +72,9 @@ public final class CompositeApiProcessor extends AbstractSalesforceProcessor {
             case COMPOSITE_TREE:
                 return processInternal(SObjectTree.class, exchange, compositeClient::submitCompositeTree,
                     this::processCompositeTreeResponse, callback);
+            case COMPOSITE_BATCH:
+                return processInternal(SObjectBatch.class, exchange, compositeClient::submitCompositeBatch,
+                    this::processCompositeBatchResponse, callback);
             default:
                 throw new SalesforceException("Unknown operation name: " + operationName.value(), null);
             }
@@ -92,6 +97,25 @@ public final class CompositeApiProcessor extends AbstractSalesforceProcessor {
         ServiceHelper.stopService(compositeClient);
     }
 
+    void processCompositeBatchResponse(final Exchange exchange, final Optional<SObjectBatchResponse> 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 SObjectBatchResponse response = responseBody.get();
+
+                out.copyFromWithNewBody(in, response);
+            }
+        } finally {
+            // notify callback that exchange is done
+            callback.done(false);
+        }
+    }
+
     void processCompositeTreeResponse(final Exchange exchange, final Optional<SObjectTreeResponse> responseBody,
         final SalesforceException exception, final AsyncCallback callback) {
 

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/AbstractSalesforceTestBase.java
----------------------------------------------------------------------
diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/AbstractSalesforceTestBase.java b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/AbstractSalesforceTestBase.java
index 3dbd36a..31bf180 100644
--- a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/AbstractSalesforceTestBase.java
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/AbstractSalesforceTestBase.java
@@ -44,7 +44,7 @@ public abstract class AbstractSalesforceTestBase extends CamelTestSupport {
         // create the component
         SalesforceComponent component = new SalesforceComponent();
         final SalesforceEndpointConfig config = new SalesforceEndpointConfig();
-        config.setApiVersion(System.getProperty("apiVersion", SalesforceEndpointConfig.DEFAULT_VERSION));
+        config.setApiVersion(System.getProperty("apiVersion", salesforceApiVersionToUse()));
         component.setConfig(config);
         component.setLoginConfig(LoginConfigHelper.getLoginConfig());
 
@@ -64,4 +64,8 @@ public abstract class AbstractSalesforceTestBase extends CamelTestSupport {
         context().addComponent("salesforce", component);
     }
 
+    protected String salesforceApiVersionToUse() {
+        return SalesforceEndpointConfig.DEFAULT_VERSION;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiBatchIntegrationTest.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..946ba5d
--- /dev/null
+++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/CompositeApiBatchIntegrationTest.java
@@ -0,0 +1,338 @@
+/**
+ * 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.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
+
+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.SObjectBatch;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatch.Method;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResponse;
+import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResult;
+import org.apache.camel.component.salesforce.dto.generated.Account;
+import org.junit.After;
+import org.junit.Before;
+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 CompositeApiBatchIntegrationTest 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 String V34 = "34.0";
+
+    private String accountId;
+
+    private final String batchuri;
+
+    public CompositeApiBatchIntegrationTest(final String format) {
+        this.batchuri = "salesforce:composite-batch?format=" + format;
+    }
+
+    @Parameters(name = "format = {0}")
+    public static Iterable<String> formats() {
+        return Arrays.asList("JSON", "XML");
+    }
+
+    @After
+    public void removeRecords() {
+        template.sendBody("salesforce:deleteSObject?sObjectName=Account&sObjectId=" + accountId, null);
+
+        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 SObjectBatch batch = new SObjectBatch(V34);
+
+        final Account updates = new Account();
+        updates.setName("NewName");
+        batch.addUpdate("Account", accountId, updates);
+
+        final Account newAccount = new Account();
+        newAccount.setName("Account created from Composite batch API");
+        batch.addCreate(newAccount);
+
+        batch.addGet("Account", accountId, "Name", "BillingPostalCode");
+
+        batch.addDelete("Account", accountId);
+
+        final SObjectBatchResponse response = template.requestBody(batchuri, batch, SObjectBatchResponse.class);
+
+        assertNotNull("Response should be provided", response);
+
+        assertFalse(response.hasErrors());
+    }
+
+    @Test
+    public void shouldSupportGenericBatchRequests() {
+        final SObjectBatch batch = new SObjectBatch(V34);
+
+        batch.addGeneric(Method.GET, "/sobjects/Account/" + accountId);
+
+        testBatch(batch);
+    }
+
+    @Test
+    public void shouldSupportLimits() {
+        final SObjectBatch batch = new SObjectBatch(V34);
+
+        batch.addLimits();
+
+        final SObjectBatchResponse response = testBatch(batch);
+
+        final List<SObjectBatchResult> results = response.getResults();
+        final SObjectBatchResult batchResult = results.get(0);
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
+
+        // JSON and XML structure are different, XML has `LimitsSnapshot` node, JSON does not
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> limits = (Map<String, Object>) result.getOrDefault("LimitsSnapshot", result);
+
+        @SuppressWarnings("unchecked")
+        final Map<String, String> apiRequests = (Map<String, String>) limits.get("DailyApiRequests");
+
+        // for JSON value will be Integer, for XML (no type information) it will be String
+        assertEquals("15000", String.valueOf(apiRequests.get("Max")));
+    }
+
+    @Test
+    public void shouldSupportObjectCreation() {
+        final SObjectBatch batch = new SObjectBatch(V34);
+
+        final Account newAccount = new Account();
+        newAccount.setName("Account created from Composite batch API");
+        batch.addCreate(newAccount);
+
+        final SObjectBatchResponse response = testBatch(batch);
+
+        final List<SObjectBatchResult> results = response.getResults();
+
+        final SObjectBatchResult batchResult = results.get(0);
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
+
+        // JSON and XML structure are different, XML has `Result` node, JSON does not
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> creationOutcome = (Map<String, Object>) result.getOrDefault("Result", result);
+
+        assertNotNull(creationOutcome.get("id"));
+    }
+
+    @Test
+    public void shouldSupportObjectDeletion() {
+        final SObjectBatch batch = new SObjectBatch(V34);
+
+        batch.addDelete("Account", accountId);
+
+        testBatch(batch);
+    }
+
+    @Test
+    public void shouldSupportObjectRetrieval() {
+        final SObjectBatch batch = new SObjectBatch(V34);
+
+        batch.addGet("Account", accountId, "Name");
+
+        final SObjectBatchResponse response = testBatch(batch);
+
+        final List<SObjectBatchResult> results = response.getResults();
+        final SObjectBatchResult batchResult = results.get(0);
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
+
+        // JSON and XML structure are different, XML has `Account` node, JSON does not
+        @SuppressWarnings("unchecked")
+        final Map<String, String> data = (Map<String, String>) result.getOrDefault("Account", result);
+
+        assertEquals("Composite API Batch", data.get("Name"));
+    }
+
+    @Test
+    public void shouldSupportObjectUpdates() {
+        final SObjectBatch batch = new SObjectBatch(V34);
+
+        final Account updates = new Account();
+        updates.setName("NewName");
+        updates.setAccountNumber("AC12345");
+        batch.addUpdate("Account", accountId, updates);
+
+        testBatch(batch);
+    }
+
+    @Test
+    public void shouldSupportQuery() {
+        final SObjectBatch batch = new SObjectBatch(V34);
+
+        batch.addQuery("SELECT Id, Name FROM Account");
+
+        final SObjectBatchResponse response = testBatch(batch);
+
+        final List<SObjectBatchResult> results = response.getResults();
+        final SObjectBatchResult batchResult = results.get(0);
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
+
+        // JSON and XML structure are different, XML has `QueryResult` node, JSON does not
+        @SuppressWarnings("unchecked")
+        final Map<String, String> data = (Map<String, String>) result.getOrDefault("QueryResult", result);
+
+        assertNotNull(data.get("totalSize"));
+    }
+
+    @Test
+    public void shouldSupportQueryAll() {
+        final SObjectBatch batch = new SObjectBatch(V34);
+
+        batch.addQueryAll("SELECT Id, Name FROM Account");
+
+        final SObjectBatchResponse response = testBatch(batch);
+
+        final List<SObjectBatchResult> results = response.getResults();
+        final SObjectBatchResult batchResult = results.get(0);
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
+
+        // JSON and XML structure are different, XML has `QueryResult` node, JSON does not
+        @SuppressWarnings("unchecked")
+        final Map<String, String> data = (Map<String, String>) result.getOrDefault("QueryResult", result);
+
+        assertNotNull(data.get("totalSize"));
+    }
+
+    @Test
+    public void shouldSupportRelatedObjectRetrieval() throws IOException {
+        final SObjectBatch batch = new SObjectBatch("36.0");
+
+        batch.addGetRelated("Account", accountId, "CreatedBy");
+
+        final SObjectBatchResponse response = testBatch(batch);
+
+        final List<SObjectBatchResult> results = response.getResults();
+        final SObjectBatchResult batchResult = results.get(0);
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
+
+        // JSON and XML structure are different, XML has `User` node, JSON does not
+        @SuppressWarnings("unchecked")
+        final Map<String, String> data = (Map<String, String>) result.getOrDefault("User", result);
+
+        final SalesforceLoginConfig loginConfig = LoginConfigHelper.getLoginConfig();
+
+        assertEquals(loginConfig.getUserName(), data.get("Username"));
+    }
+
+    @Test
+    public void shouldSupportSearch() {
+        final SObjectBatch batch = new SObjectBatch(V34);
+
+        batch.addSearch("FIND {Batch} IN Name Fields RETURNING Account (Name) ");
+
+        final SObjectBatchResponse response = testBatch(batch);
+
+        final List<SObjectBatchResult> results = response.getResults();
+        final SObjectBatchResult batchResult = results.get(0);
+
+        final Object firstBatchResult = batchResult.getResult();
+
+        final Map<String, Object> result;
+        if (firstBatchResult instanceof List) {
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> tmp = (Map<String, Object>) ((List) firstBatchResult).get(0);
+            result = tmp;
+        } else {
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> tmp = (Map<String, Object>) firstBatchResult;
+            result = tmp;
+        }
+
+        // JSON and XML structure are different, XML has `SearchResults` node, JSON does not
+        @SuppressWarnings("unchecked")
+        final Map<String, String> data = (Map<String, String>) result.getOrDefault("SearchResults", result);
+
+        assertNotNull(data.get("Name"));
+    }
+
+    SObjectBatchResponse testBatch(final SObjectBatch batch) {
+        final SObjectBatchResponse response = template.requestBody(batchuri, batch, SObjectBatchResponse.class);
+
+        assertNotNull("Response should be provided", response);
+
+        assertFalse("Received errors in: " + response, response.hasErrors());
+
+        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 "37.0";
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/65b7ac72/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 d996325..132de83 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
@@ -108,7 +108,8 @@ 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", "composite-tree", "[PushTopicName]"
+            "getReportInstances", "getReportResults", "limits", "approval", "approvals", "composite-batch", "composite-tree",
+            "[PushTopicName]"
         );
 
         // get filtered operation names


Mime
View raw message