CAMEL-10472 Update Salesforce component to support recent items REST API This commit adds support for getting recent items via Salesforce REST API[1].
New operation `recent` was added and can be used like: ...to("salesforce:recent") .split().body() .log("${body.name} at ${body.attributes.url}"); Number of items returned can be limited by the `limit` parameter in URI, body or headers. [1] https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_recent_items.htm Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/4e3b2f7a Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/4e3b2f7a Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/4e3b2f7a Branch: refs/heads/master Commit: 4e3b2f7af9ed3d7c99fcfd32f0c84c37838ea19f Parents: a52ab3c Author: Zoran Regvart <zo...@regvart.com> Authored: Tue Nov 22 18:24:32 2016 +0100 Committer: Claus Ibsen <davscl...@apache.org> Committed: Wed Nov 23 09:44:01 2016 +0100 ---------------------------------------------------------------------- .../salesforce/SalesforceEndpointConfig.java | 17 +++ .../salesforce/api/TypeReferences.java | 4 + .../salesforce/internal/OperationName.java | 1 + .../internal/client/DefaultRestClient.java | 13 ++ .../salesforce/internal/client/RestClient.java | 12 ++ .../processor/AbstractRestProcessor.java | 10 ++ .../internal/processor/JsonRestProcessor.java | 5 + .../salesforce/RecentIntegrationTest.java | 140 +++++++++++++++++++ .../salesforce/api/dto/RecentItemTest.java | 65 +++++++++ 9 files changed, 267 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/4e3b2f7a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpointConfig.java ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpointConfig.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpointConfig.java index ad18aab..3d59a19 100644 --- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpointConfig.java +++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/SalesforceEndpointConfig.java @@ -56,6 +56,7 @@ public class SalesforceEndpointConfig implements Cloneable { public static final String SOBJECT_SEARCH = "sObjectSearch"; public static final String APEX_METHOD = "apexMethod"; public static final String APEX_URL = "apexUrl"; + public static final String LIMIT = "limit"; // prefix for parameters in headers public static final String APEX_QUERY_PARAM_PREFIX = "apexQueryParam."; @@ -173,6 +174,9 @@ public class SalesforceEndpointConfig implements Cloneable { @UriParam private long maxBackoff = DEFAULT_MAX_BACKOFF; + @UriParam + private Integer limit; + public SalesforceEndpointConfig copy() { try { final SalesforceEndpointConfig copy = (SalesforceEndpointConfig) super.clone(); @@ -563,6 +567,7 @@ public class SalesforceEndpointConfig implements Cloneable { valueMap.put(SOBJECT_SEARCH, sObjectSearch); valueMap.put(APEX_METHOD, apexMethod); valueMap.put(APEX_URL, apexUrl); + valueMap.put(LIMIT, limit); // apexQueryParams are handled explicitly in AbstractRestProcessor // add bulk API properties @@ -608,4 +613,16 @@ public class SalesforceEndpointConfig implements Cloneable { public void setInitialReplayIdMap(Map<String, Integer> initialReplayIdMap) { this.initialReplayIdMap = initialReplayIdMap; } + + public Integer getLimit() { + return limit; + } + + /** + * Limit on number of returned records. Applicable to some of the API, check the Salesforce documentation. + * @param limit + */ + public void setLimit(final Integer limit) { + this.limit = limit; + } } http://git-wip-us.apache.org/repos/asf/camel/blob/4e3b2f7a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/TypeReferences.java ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/TypeReferences.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/TypeReferences.java index 42f022d..7128312 100644 --- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/TypeReferences.java +++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/TypeReferences.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import org.apache.camel.component.salesforce.api.dto.Limits.Operation; import org.apache.camel.component.salesforce.api.dto.Limits.Usage; +import org.apache.camel.component.salesforce.api.dto.RecentItem; import org.apache.camel.component.salesforce.api.dto.RestError; import org.apache.camel.component.salesforce.api.dto.SearchResult; import org.apache.camel.component.salesforce.api.dto.Version; @@ -55,6 +56,9 @@ public final class TypeReferences { public static final TypeReference<List<SearchResult>> SEARCH_RESULT_TYPE = new TypeReference<List<SearchResult>>() { }; + public static final TypeReference<List<RecentItem>> RECENT_ITEM_LIST_TYPE = new TypeReference<List<RecentItem>>() { + }; + private TypeReferences() { // not meant for instantiation, only for TypeReference constants } http://git-wip-us.apache.org/repos/asf/camel/blob/4e3b2f7a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/OperationName.java ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/OperationName.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/OperationName.java index 15fdf0b..7570374 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 @@ -37,6 +37,7 @@ public enum OperationName { QUERY_ALL("queryAll"), SEARCH("search"), APEX_CALL("apexCall"), + RECENT("recent"), // bulk API CREATE_JOB("createJob"), http://git-wip-us.apache.org/repos/asf/camel/blob/4e3b2f7a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/DefaultRestClient.java ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/DefaultRestClient.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/DefaultRestClient.java index d1a5394..f604312 100644 --- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/DefaultRestClient.java +++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/DefaultRestClient.java @@ -23,6 +23,7 @@ import java.net.URISyntaxException; import java.net.URLEncoder; import java.util.List; import java.util.Map; +import java.util.Optional; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -403,6 +404,18 @@ public class DefaultRestClient extends AbstractClientBase implements RestClient } @Override + public void recent(final Integer limit, final ResponseCallback responseCallback) { + final String param = Optional.ofNullable(limit).map(v -> "?limit=" + v).orElse(""); + + final Request get = getRequest(HttpMethod.GET, versionUrl() + "recent/" + param); + + // requires authorization token + setAccessToken(get); + + doHttpRequest(get, new DelegatingClientCallback(responseCallback)); + } + + @Override public void limits(final ResponseCallback responseCallback) { final Request get = getRequest(HttpMethod.GET, versionUrl() + "limits/"); http://git-wip-us.apache.org/repos/asf/camel/blob/4e3b2f7a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/RestClient.java ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/RestClient.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/RestClient.java index 470aa33..55764d0 100644 --- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/RestClient.java +++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/client/RestClient.java @@ -195,6 +195,18 @@ public interface RestClient { ResponseCallback callback); /** + * Fetches recently viewed records. + * + * @param limit + * optional limit that specifies the maximum number of records to be returned. If this parameter is not + * specified, the default maximum number of records returned is the maximum number of entries in + * RecentlyViewed, which is 200 records per object. + * @param responseCallback + * {@link ResponseCallback} to handle response or exception + */ + void recent(Integer limit, ResponseCallback responseCallback); + + /** * Fetches Organization Limits. * * @param responseCallback {@link ResponseCallback} to handle response or exception http://git-wip-us.apache.org/repos/asf/camel/blob/4e3b2f7a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/AbstractRestProcessor.java ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/AbstractRestProcessor.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/AbstractRestProcessor.java index 35e0aff..6636ada 100644 --- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/AbstractRestProcessor.java +++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/AbstractRestProcessor.java @@ -29,6 +29,7 @@ import java.util.regex.Pattern; import org.apache.camel.AsyncCallback; import org.apache.camel.Exchange; import org.apache.camel.component.salesforce.SalesforceEndpoint; +import org.apache.camel.component.salesforce.SalesforceEndpointConfig; import org.apache.camel.component.salesforce.api.SalesforceException; import org.apache.camel.component.salesforce.api.dto.AbstractSObjectBase; import org.apache.camel.component.salesforce.internal.PayloadFormat; @@ -152,6 +153,9 @@ public abstract class AbstractRestProcessor extends AbstractSalesforceProcessor case APEX_CALL: processApexCall(exchange, callback); break; + case RECENT: + processRecent(exchange, callback); + break; case LIMITS: processLimits(exchange, callback); break; @@ -564,6 +568,12 @@ public abstract class AbstractRestProcessor extends AbstractSalesforceProcessor return apexUrl; } + private void processRecent(Exchange exchange, AsyncCallback callback) throws SalesforceException { + final Integer limit = getParameter(SalesforceEndpointConfig.LIMIT, exchange, true, true, Integer.class); + + restClient.recent(limit, (response, exception) -> processResponse(exchange, response, exception, callback)); + } + private void processLimits(Exchange exchange, AsyncCallback callback) { restClient.limits((response, exception) -> processResponse(exchange, response, exception, callback)); } http://git-wip-us.apache.org/repos/asf/camel/blob/4e3b2f7a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/JsonRestProcessor.java ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/JsonRestProcessor.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/JsonRestProcessor.java index 5fe991f..a0c50b3 100644 --- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/JsonRestProcessor.java +++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/internal/processor/JsonRestProcessor.java @@ -103,6 +103,11 @@ public class JsonRestProcessor extends AbstractRestProcessor { exchange.setProperty(RESPONSE_TYPE, TypeReferences.SEARCH_RESULT_TYPE); break; + case RECENT: + // handle known response type + exchange.setProperty(RESPONSE_TYPE, TypeReferences.RECENT_ITEM_LIST_TYPE); + break; + case LIMITS: // handle known response type exchange.setProperty(RESPONSE_CLASS, Limits.class); http://git-wip-us.apache.org/repos/asf/camel/blob/4e3b2f7a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/RecentIntegrationTest.java ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/RecentIntegrationTest.java b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/RecentIntegrationTest.java new file mode 100644 index 0000000..3774295 --- /dev/null +++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/RecentIntegrationTest.java @@ -0,0 +1,140 @@ +/** + * 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.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.thoughtworks.xstream.annotations.XStreamImplicit; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.salesforce.api.dto.AbstractQueryRecordsBase; +import org.apache.camel.component.salesforce.api.dto.RecentItem; +import org.apache.camel.component.salesforce.dto.generated.Account; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class RecentIntegrationTest 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 Object NOT_USED = null; + + static Account account(final int ord) { + final Account account = new Account(); + account.setName("recent-" + ord); + + return account; + } + + static void assertRecentItemsSize(final List<RecentItem> items, final int expected) { + final List<RecentItem> recentItems = items.stream().filter(i -> i.getName().startsWith("recent-")) + .collect(Collectors.toList()); + + assertListSize("Expected " + expected + " items named `recent-N` in recent items", recentItems, expected); + } + + @After + public void deleteRecords() { + template.sendBody("direct:delete-recent", NOT_USED); + } + + @Before + public void setupTenRecentItems() { + final List<Account> accounts = IntStream.range(0, 10).mapToObj(RecentIntegrationTest::account) + .collect(Collectors.toList()); + + template.sendBody("direct:create-recent", accounts); + } + + @Test + public void shouldFetchRecentItems() { + @SuppressWarnings("unchecked") + final List<RecentItem> items = template.requestBody("direct:test-recent", NOT_USED, List.class); + + assertRecentItemsSize(items, 10); + } + + @Test + public void shouldFetchRecentItemsLimitingByHeaderParam() { + @SuppressWarnings("unchecked") + final List<RecentItem> items = template.requestBody("direct:test-recent-with-header-limit-param", NOT_USED, + List.class); + + assertRecentItemsSize(items, 5); + } + + @Test + public void shouldFetchRecentItemsLimitingByParamInBody() { + @SuppressWarnings("unchecked") + final List<RecentItem> items = template.requestBody("direct:test-recent-with-body-limit-param", NOT_USED, + List.class); + + assertRecentItemsSize(items, 5); + } + + @Test + public void shouldFetchRecentItemsLimitingByUriParam() { + @SuppressWarnings("unchecked") + final List<RecentItem> items = template.requestBody("direct:test-recent-with-limit-uri-param", NOT_USED, + List.class); + + assertRecentItemsSize(items, 5); + } + + @Override + protected RouteBuilder doCreateRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:create-recent").split().body().to("salesforce:createSObject?sObjectName=Account").end() + .to("salesforce:query?sObjectClass=" + Accounts.class.getName() + + "&sObjectQuery=SELECT Id FROM Account WHERE Name LIKE 'recent-%' FOR VIEW"); + + from("direct:delete-recent") + .to("salesforce:query?sObjectClass=" + Accounts.class.getName() + + "&sObjectQuery=SELECT Id FROM Account WHERE Name LIKE 'recent-%'") + .transform(simple("${body.records}")).split().body() + .setHeader(SalesforceEndpointConfig.SOBJECT_ID).simple("${body.id}") + .to("salesforce:deleteSObject?sObjectName=Account"); + + from("direct:test-recent").to("salesforce:recent"); + + from("direct:test-recent-with-limit-uri-param").to("salesforce:recent?limit=5"); + + from("direct:test-recent-with-header-limit-param").setHeader(SalesforceEndpointConfig.LIMIT).constant(5) + .to("salesforce:recent"); + + from("direct:test-recent-with-body-limit-param").setBody(constant(5)).to("salesforce:recent"); + } + }; + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/4e3b2f7a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/RecentItemTest.java ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/RecentItemTest.java b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/RecentItemTest.java new file mode 100644 index 0000000..3fc16ee --- /dev/null +++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/dto/RecentItemTest.java @@ -0,0 +1,65 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.salesforce.api.dto; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.Test; + +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +public class RecentItemTest { + + @Test + public void shouldDeserializeFromJSON() throws JsonProcessingException, IOException { + final ObjectMapper mapper = new ObjectMapper(); + + final Object read = mapper.readerFor(RecentItem.class) + .readValue("{ \n" + // + " \"attributes\" : \n" + // + " { \n" + // + " \"type\" : \"Account\", \n" + // + " \"url\" : \"/services/data/v28.0/sobjects/Account/a06U000000CelH0IAJ\" \n" + // + " }, \n" + // + " \"Id\" : \"a06U000000CelH0IAJ\", \n" + // + " \"Name\" : \"Acme\" \n" + // + "}"); + + assertThat("RecentItem should deserialize from JSON", read, instanceOf(RecentItem.class)); + + RecentItem recentItem = (RecentItem) read; + + assertEquals("RecentItem.Id should be deserialized", recentItem.getId(), "a06U000000CelH0IAJ"); + + assertEquals("RecentItem.Name should be deserialized", recentItem.getName(), "Acme"); + + assertNotNull("RecentItem.attributes should be deserialized", recentItem.getAttributes()); + + assertEquals("RecentItem.attributes.type should be deserialized", recentItem.getAttributes().getType(), + "Account"); + + assertEquals("RecentItem.attributes.url should be deserialized", recentItem.getAttributes().getUrl(), + "/services/data/v28.0/sobjects/Account/a06U000000CelH0IAJ"); + + } +}