[
https://issues.apache.org/jira/browse/CAMEL-11995?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16243714#comment-16243714
]
ASF GitHub Bot commented on CAMEL-11995:
----------------------------------------
zregvart closed pull request #2085: CAMEL-11995: Salesforce Composite API
support
URL: https://github.com/apache/camel/pull/2085
This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:
As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):
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 b50f940935e..11ac478fa2e 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 f75e2b464ae..e41cf6334b2 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 @@
+
"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 5c9c7e31226..e332386309b 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 SalesforceProducer(SalesforceEndpoint endpoint) throws
SalesforceExceptio
}
}
- private boolean isBulkOperation(OperationName operationName) {
+ private static boolean isBulkOperation(OperationName operationName) {
switch (operationName) {
case CREATE_JOB:
case GET_JOB:
@@ -82,7 +82,7 @@ private boolean isBulkOperation(OperationName operationName) {
}
}
- 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 @@ private boolean isAnalyticsOperation(OperationName
operationName) {
}
}
- 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 00000000000..72ffe7a7078
--- /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 00000000000..864b2c4b05b
--- /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 00000000000..a5abaf04776
--- /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 00000000000..5ea28025d50
--- /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 736d9174fb3..4501d5cafdc 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 @@
/**
* 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 3f18dbc95be..fb18971c4d8 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 @@
// 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 5bab28b3bb2..a399faad296 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 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 @@
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 64b170b3c2a..9efc74bf439 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.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 @@
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 @@
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 DefaultCompositeApiClient(final
SalesforceEndpointConfig configuration, f
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 void submitCompositeBatch(final SObjectBatch
batch, final Map<String, Lis
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 void submitCompositeTree(final SObjectTree tree,
final Map<String, List<S
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 @@ Request createRequest(final HttpMethod method, final
String url, Map<String, Lis
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 @@ ObjectWriter jsonWriterFor(final Object obj) {
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 @@ InputStream toJson(final Object obj) throws
SalesforceException {
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 @@ String versionUrl() {
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 @@ protected void setAccessToken(final Request request) {
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 8f54431b053..cea51fa61f7 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 @@
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 @@
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 AbstractSalesforceProcessor(SalesforceEndpoint
endpoint) {
* @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 @@ protected final String getParameter(String propName, Exchange
exchange, boolean
* @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 @@ protected final String getParameter(String propName,
Exchange exchange, boolean
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 @@ protected final String getParameter(String propName,
Exchange exchange, boolean
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 188e777056a..966a9b9eb52 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.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 @@
@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 boolean process(final Exchange exchange, final
AsyncCallback callback) {
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 @@ void processCompositeBatchResponse(final Exchange
exchange, final Optional<SObje
}
}
+ 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 @@ void processCompositeTreeResponse(final Exchange exchange,
final Optional<SObjec
}
}
- 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 @@ boolean processException(final Exchange exchange, final
AsyncCallback callback,
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 b70b87f3fd3..5bfadd097b1 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 void shouldSupportGenericBatchRequests() {
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 00000000000..8f59cf888d5
--- /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 00000000000..d89e2c7812a
--- /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 00000000000..5345875162a
--- /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 00000000000..513e352dced
--- /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 00000000000..439e9f2d932
--- /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 00000000000..4af33bf7d72
--- /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
----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
For queries about this service, please contact Infrastructure at:
[email protected]
> Support Composite API
> ---------------------
>
> Key: CAMEL-11995
> URL: https://issues.apache.org/jira/browse/CAMEL-11995
> Project: Camel
> Issue Type: New Feature
> Components: camel-salesforce
> Reporter: Zoran Regvart
> Assignee: Zoran Regvart
> Fix For: 2.21.0
>
>
> Camel Salesforce component supports Composite Tree and Composite Batch APIs,
> there is a new [sub-API called simply
> Composite|https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_composite.htm]
> for executing multiple REST calls (up to 25) in a single request.
--
This message was sent by Atlassian JIRA
(v6.4.14#64029)