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 12:10:44 UTC
[2/2] camel git commit: CAMEL-10593 Support for Composite API batch
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 <zo...@regvart.com>
Authored: Fri Dec 9 12:13:46 2016 +0100
Committer: Claus Ibsen <da...@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