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}