Salesforce Connect の Google ブックス™ カスタムアダプター
この例は、外部システムの API (この場合は Google ブックス API ファミリー) の要件および制限に対処する方法を示しています。
Google ブックス™ サービスと統合するには、Salesforce Connect を次のように設定します。
- Google ブックス API では最大 40 件の結果を返すことができるため、41 行以上の結果セットを処理するカスタムアダプターを開発します。
- Google ブックス API は、検索の関連性と公開日でしか並べ替えができないため、列での並べ替えを無効にするカスタムアダプターを開発します。
- OAuth をサポートするために、Salesforce で認証設定を行うときに、アクセストークンの要求権限範囲に https://www.googleapis.com/auth/books が含まれるようにします。
- Apex コールアウトを許可するには、Salesforce に次のリモートサイトを定義します。
- https://www.googleapis.com
- https://books.google.com
BooksDataSourceConnection クラス
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 クラス
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}