Note: This release is in preview. Features described here don’t become generally available until the latest general availability date that Salesforce announces for this release. Before then, and where features are noted as beta, pilot, or developer preview, we can’t guarantee general availability within any particular time frame or at all. Make your purchase decisions only on the basis of generally available products and features.
Google Books™ Custom Adapter for Salesforce Connect
This example illustrates how to work around the requirements and limits of an
external system’s APIs: in this case, the Google Books API Family.
To integrate with the Google Books™ service, we set up Salesforce Connect as follows.
- The Google Books API allows a maximum of 40 returned results, so we develop our custom adapter to handle result sets with more than 40 rows.
- The Google Books API can sort only by search relevance and publish dates, so we develop our custom adapter to disable sorting on columns.
- To support OAuth, we set up our authentication settings in Salesforce so that the requested scope of permissions for access tokens includes https://www.googleapis.com/auth/books.
- To allow Apex callouts, we define these remote sites in Salesforce:
- https://www.googleapis.com
- https://books.google.com
BooksDataSourceConnection Class
1/**
2 * Extends the DataSource.Connection class to enable
3 * Salesforce to sync the external system metadata
4 * schema and to handle queries and searches of the external
5 * data.
6 **/
7global class BooksDataSourceConnection extends
8 DataSource.Connection {
9
10 private DataSource.ConnectionParams connectionInfo;
11
12 // Constructor for BooksDataSourceConnection.
13 global BooksDataSourceConnection(DataSource.ConnectionParams
14 connectionInfo) {
15 this.connectionInfo = connectionInfo;
16 }
17
18 /**
19 * Called when an external object needs to get a list of
20 * schema from the external data source, for example when
21 * the administrator clicks “Validate and Sync” in the
22 * user interface for the external data source.
23 **/
24 override global List<DataSource.Table> sync() {
25 List<DataSource.Table> tables =
26 new List<DataSource.Table>();
27 List<DataSource.Column> columns;
28 columns = new List<DataSource.Column>();
29 columns.add(getColumn('title'));
30 columns.add(getColumn('description'));
31 columns.add(getColumn('publishedDate'));
32 columns.add(getColumn('publisher'));
33 columns.add(DataSource.Column.url('DisplayUrl'));
34 columns.add(DataSource.Column.text('ExternalId', 255));
35 tables.add(DataSource.Table.get('googleBooks', 'title',
36 columns));
37 return tables;
38 }
39
40 /**
41 * Google Books API v1 doesn't support sorting,
42 * so we create a column with sortable = false.
43 **/
44 private DataSource.Column getColumn(String columnName) {
45 DataSource.Column column = DataSource.Column.text(columnName,
46 255);
47 column.sortable = false;
48 return column;
49 }
50
51 /**
52 * Called to query and get results from the external
53 * system for SOQL queries, list views, and detail pages
54 * for an external object that's associated with the
55 * external data source.
56 *
57 * The QueryContext argument represents the query to run
58 * against a table in the external system.
59 *
60 * Returns a list of rows as the query results.
61 **/
62 override global DataSource.TableResult query(
63 DataSource.QueryContext contexts) {
64 DataSource.Filter filter = contexts.tableSelection.filter;
65 String url;
66 if (contexts.tableSelection.columnsSelected.size() == 1 &&
67 contexts.tableSelection.columnsSelected.get(0).aggregation ==
68 DataSource.QueryAggregation.COUNT) {
69 return getCount(contexts);
70 }
71
72 if (filter != null) {
73 String thisColumnName = filter.columnName;
74 if (thisColumnName != null &&
75 thisColumnName.equals('ExternalId')) {
76 url = 'https://www.googleapis.com/books/v1/' +
77 'volumes?q=' + filter.columnValue +
78 '&maxResults=1&id=' + filter.columnValue;
79 return DataSource.TableResult.get(true, null,
80 contexts.tableSelection.tableSelected,
81 getData(url));
82 }
83 else {
84 url = 'https://www.googleapis.com/books/' +
85 'v1/volumes?q=' + filter.columnValue +
86 '&id=' + filter.columnValue +
87 '&maxResults=40' + '&startIndex=';
88 }
89 } else {
90 url = 'https://www.googleapis.com/books/v1/' +
91 'volumes?q=america&' + '&maxResults=40' +
92 '&startIndex=';
93 }
94 /**
95 * Google Books API v1 supports maxResults of 40
96 * so we handle pagination explicitly in the else statement
97 * when we handle more than 40 records per query.
98 **/
99 if (contexts.maxResults < 40) {
100 return DataSource.TableResult.get(true, null,
101 contexts.tableSelection.tableSelected,
102 getData(url + contexts.offset));
103 }
104 else {
105 return fetchData(contexts, url);
106 }
107 }
108
109 /**
110 * Helper method to fetch results when maxResults is
111 * greater than 40 (the max value for maxResults supported
112 * by Google Books API v1).
113 **/
114 private DataSource.TableResult fetchData(
115 DataSource.QueryContext contexts, String url) {
116 Integer fetchSlot = (contexts.maxResults / 40) + 1;
117 List<Map<String, Object>> data =
118 new List<Map<String, Object>>();
119 Integer startIndex = contexts.offset;
120 for(Integer count = 0; count < fetchSlot; count++) {
121 data.addAll(getData(url + startIndex));
122 if(count == 0)
123 contexts.offset = 41;
124 else
125 contexts.offset += 40;
126 }
127
128 return DataSource.TableResult.get(true, null,
129 contexts.tableSelection.tableSelected, data);
130 }
131
132 /**
133 * Helper method to execute count() query.
134 **/
135 private DataSource.TableResult getCount(
136 DataSource.QueryContext contexts) {
137 String url = 'https://www.googleapis.com/books/v1/' +
138 'volumes?q=america&projection=full';
139 List<Map<String,Object>> response =
140 DataSource.QueryUtils.filter(contexts, getData(url));
141 List<Map<String, Object>> countResponse =
142 new List<Map<String, Object>>();
143 Map<String, Object> countRow =
144 new Map<String, Object>();
145 countRow.put(
146 contexts.tableSelection.columnsSelected.get(0).columnName,
147 response.size());
148 countResponse.add(countRow);
149 return DataSource.TableResult.get(contexts, countResponse);
150 }
151
152 /**
153 * Called to do a full text search and get results from
154 * the external system for SOSL queries and Salesforce
155 * global searches.
156 *
157 * The SearchContext argument represents the query to run
158 * against a table in the external system.
159 *
160 * Returns results for each table that the SearchContext
161 * requested to be searched.
162 **/
163 override global List<DataSource.TableResult> search(
164 DataSource.SearchContext contexts) {
165 List<DataSource.TableResult> results =
166 new List<DataSource.TableResult>();
167
168 for (Integer i =0; i< contexts.tableSelections.size();i++) {
169 String entity = contexts.tableSelections[i].tableSelected;
170 String url = 'https://www.googleapis.com/books/v1' +
171 '/volumes?q=' + contexts.searchPhrase;
172 results.add(DataSource.TableResult.get(true, null,
173 entity,
174 getData(url)));
175 }
176
177 return results;
178 }
179
180 /**
181 * Helper method to parse the data.
182 * Returns a list of rows from the external system.
183 **/
184 public List<Map<String, Object>> getData(String url) {
185 HttpResponse response = getResponse(url);
186 String body = response.getBody();
187
188 List<Map<String, Object>> rows =
189 new List<Map<String, Object>>();
190
191 Map<String, Object> responseBodyMap =
192 (Map<String, Object>)JSON.deserializeUntyped(body);
193
194 /**
195 * Checks errors.
196 **/
197 Map<String, Object> error =
198 (Map<String, Object>)responseBodyMap.get('error');
199 if (error!=null) {
200 List<Object> errorsList =
201 (List<Object>)error.get('errors');
202 Map<String, Object> errors =
203 (Map<String, Object>)errorsList[0];
204 String messages = (String)errors.get('message');
205 throw new DataSource.OAuthTokenExpiredException(messages);
206 }
207
208 List<Object> sItems = (List<Object>)responseBodyMap.get('items');
209 if (sItems != null) {
210 for (Integer i=0; i< sItems.size(); i++) {
211 Map<String, Object> item =
212 (Map<String, Object>)sItems[i];
213 rows.add(createRow(item));
214 }
215 } else {
216 rows.add(createRow(responseBodyMap));
217 }
218
219 return rows;
220 }
221
222 /**
223 * Helper method to populate a row based on source data.
224 *
225 * The item argument maps to the data that
226 * represents a row.
227 *
228 * Returns an updated map with the External ID and
229 * Display URL values.
230 **/
231 public Map<String, Object> createRow(
232 Map<String, Object> item) {
233 Map<String, Object> row = new Map<String, Object>();
234 for ( String key : item.keySet() ){
235 if (key == 'id') {
236 row.put('ExternalId', item.get(key));
237 } else if (key == 'volumeInfo') {
238 Map<String, Object> volumeInfoMap =
239 (Map<String, Object>)item.get(key);
240 row.put('title', volumeInfoMap.get('title'));
241 row.put('description',
242 volumeInfoMap.get('description'));
243 row.put('DisplayUrl',
244 volumeInfoMap.get('infoLink'));
245 row.put('publishedDate',
246 volumeInfoMap.get('publishedDate'));
247 row.put('publisher',
248 volumeInfoMap.get('publisher'));
249 }
250 }
251 return row;
252 }
253
254 /**
255 * Helper method to make the HTTP GET call.
256 * The url argument is the URL of the external system.
257 * Returns the response from the external system.
258 **/
259 public HttpResponse getResponse(String url) {
260 Http httpProtocol = new Http();
261 HttpRequest request = new HttpRequest();
262 request.setEndPoint(url);
263 request.setMethod('GET');
264 request.setHeader('Authorization', 'Bearer '+
265 this.connectionInfo.oauthToken);
266 HttpResponse response = httpProtocol.send(request);
267 return response;
268 }
269}BooksDataSourceProvider Class
1/**
2 * Extends the DataSource.Provider base class to create a
3 * custom adapter for Salesforce Connect. The class informs
4 * Salesforce of the functional and authentication
5 * capabilities that are supported by or required to connect
6 * to an external system.
7 **/
8global class BooksDataSourceProvider extends
9 DataSource.Provider {
10 /**
11 * Declares the types of authentication that can be used
12 * to access the external system.
13 **/
14 override global List<DataSource.AuthenticationCapability>
15 getAuthenticationCapabilities() {
16 List<DataSource.AuthenticationCapability> capabilities =
17 new List<DataSource.AuthenticationCapability>();
18 capabilities.add(
19 DataSource.AuthenticationCapability.OAUTH);
20 capabilities.add(
21 DataSource.AuthenticationCapability.ANONYMOUS);
22 return capabilities;
23 }
24
25 /**
26 * Declares the functional capabilities that the
27 * external system supports.
28 **/
29 override global List<DataSource.Capability>
30 getCapabilities() {
31 List<DataSource.Capability> capabilities = new
32 List<DataSource.Capability>();
33 capabilities.add(DataSource.Capability.ROW_QUERY);
34 capabilities.add(DataSource.Capability.SEARCH);
35 return capabilities;
36 }
37
38 /**
39 * Declares the associated DataSource.Connection class.
40 **/
41 override global DataSource.Connection getConnection(
42 DataSource.ConnectionParams connectionParams) {
43 return new BooksDataSourceConnection(connectionParams);
44 }
45}