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