Newer Version Available

This content describes an older version of this product. View Latest

Google Drive™ Custom Adapter for Salesforce Connect

This example illustrates how to use callouts and OAuth to connect to an external system, which in this case is the Google Drive™ online storage service. The example also shows how to avoid failing tests from web service callouts by returning mock responses for test methods.

For this example to work reliably, request offline access when setting up OAuth so that Salesforce can obtain and maintain a refresh token for your connections.

DriveDataSourceConnection Class

1/**
2 *   Extends the DataSource.Connection class to enable 
3 *   Salesforce to sync the external system’s schema 
4 *   and to handle queries and searches of the external data. 
5 **/
6global class DriveDataSourceConnection extends
7    DataSource.Connection {
8    private DataSource.ConnectionParams connectionInfo;
9    
10    /**
11     *   Constructor for DriveDataSourceConnection.
12     **/
13    global DriveDataSourceConnection(
14        DataSource.ConnectionParams 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(DataSource.Column.text('title', 255));
30        columns.add(DataSource.Column.text('description',255));
31        columns.add(DataSource.Column.text('createdDate',255));
32        columns.add(DataSource.Column.text('modifiedDate',255));
33        columns.add(DataSource.Column.url('selfLink'));
34        columns.add(DataSource.Column.url('DisplayUrl'));
35        columns.add(DataSource.Column.text('ExternalId',255));
36        tables.add(DataSource.Table.get('googleDrive','title',
37            columns));
38        return tables;
39    }
40
41    /**
42     *   Called to query and get results from the external 
43     *   system for SOQL queries, list views, and detail pages 
44     *   for an external object that’s associated with the 
45     *   external data source.
46     *   
47     *   The QueryContext argument represents the query to run 
48     *   against a table in the external system.
49     *   
50     *   Returns a list of rows as the query results.
51     **/
52    override global DataSource.TableResult query(
53        DataSource.QueryContext context) {
54        DataSource.Filter filter = context.tableSelection.filter;
55        String url;
56        if (filter != null) {
57            String thisColumnName = filter.columnName;
58            if (thisColumnName != null && 
59                    thisColumnName.equals('ExternalId'))
60                url = 'https://www.googleapis.com/drive/v2/'
61                + 'files/' + filter.columnValue;
62            else
63                url = 'https://www.googleapis.com/drive/v2/'
64                + 'files';
65        } else {
66            url = 'https://www.googleapis.com/drive/v2/' 
67            + 'files';
68        }
69
70        /**
71         * Filters, sorts, and applies limit and offset clauses.
72         **/
73        List<Map<String, Object>> rows = 
74            DataSource.QueryUtils.process(context, getData(url));
75        return DataSource.TableResult.get(true, null,
76            context.tableSelection.tableSelected, rows);
77    }
78
79    /**
80     *   Called to do a full text search and get results from
81     *   the external system for SOSL queries and Salesforce
82     *   global searches.
83     *   
84     *   The SearchContext argument represents the query to run 
85     *   against a table in the external system.
86     *   
87     *   Returns results for each table that the SearchContext 
88     *   requested to be searched.
89     **/
90    override global List<DataSource.TableResult> search(
91        DataSource.SearchContext context) {
92        List<DataSource.TableResult> results =
93            new List<DataSource.TableResult>();
94
95        for (Integer i =0;i< context.tableSelections.size();i++) {
96            String entity = context.tableSelections[i].tableSelected;
97            String url = 
98                'https://www.googleapis.com/drive/v2/files'+
99                '?q=fullText+contains+\''+context.searchPhrase+'\'';
100            results.add(DataSource.TableResult.get(
101                true, null, entity, getData(url)));
102        }
103
104        return results;
105    }
106
107    /**
108     *   Helper method to parse the data.
109     *   The url argument is the URL of the external system.
110     *   Returns a list of rows from the external system.
111     **/
112    public List<Map<String, Object>> getData(String url) {
113        String response = getResponse(url);
114
115        List<Map<String, Object>> rows =
116            new List<Map<String, Object>>();
117
118        Map<String, Object> responseBodyMap = (Map<String, Object>)
119            JSON.deserializeUntyped(response);
120
121        /**
122         *   Checks errors.
123         **/
124        Map<String, Object> error =
125            (Map<String, Object>)responseBodyMap.get('error');
126        if (error!=null) {
127            List<Object> errorsList =
128                (List<Object>)error.get('errors');
129            Map<String, Object> errors =
130                (Map<String, Object>)errorsList[0];
131            String errorMessage = (String)errors.get('message');
132            throw new DataSource.OAuthTokenExpiredException(errorMessage);
133        }
134
135        List<Object> fileItems=(List<Object>)responseBodyMap.get('items');
136        if (fileItems != null) {
137            for (Integer i=0; i < fileItems.size(); i++) {
138                Map<String, Object> item = 
139                    (Map<String, Object>)fileItems[i];
140                rows.add(createRow(item));  
141            }
142        } else {
143            rows.add(createRow(responseBodyMap));
144        }
145
146        return rows;
147    }
148
149    /**
150     *   Helper method to populate the External ID and Display 
151     *   URL fields on external object records based on the 'id' 
152     *   value that’s sent by the external system.
153     *   
154     *   The Map<String, Object> item parameter maps to the data 
155     *   that represents a row.
156     *   
157     *   Returns an updated map with the External ID and 
158     *   Display URL values.
159     **/
160    public Map<String, Object> createRow(
161        Map<String, Object> item){
162        Map<String, Object> row = new Map<String, Object>();
163        for ( String key : item.keySet() ) {
164            if (key == 'id') {
165                row.put('ExternalId', item.get(key));
166            } else if (key=='selfLink') {
167                row.put(key, item.get(key));
168                row.put('DisplayUrl', item.get(key));
169            } else {
170                row.put(key, item.get(key));
171            }
172        }
173        return row;
174    }
175    
176    static String mockResponse = '{' +
177      '  "kind": "drive#file",' +
178      '  "id": “12345”,' +
179      '  "selfLink": “files/12345”,' +
180      '  "title": “Mock File”,' +
181      '  "mimeType": “application/text”,' +
182      '  "description": “Mock response that’s used during tests”,' +
183      '  "createdDate": “2016-04-20”,' +
184      '  "modifiedDate": 2016-04-20”,' +
185      '  "version": 1,' +
186      '}';
187    
188    /**
189     *   Helper method to make the HTTP GET call.
190     *   The url argument is the URL of the external system.
191     *   Returns the response from the external system.
192     **/
193    public String getResponse(String url) {
194        if (System.Test.isRunningTest()) {
195          // Avoid callouts during tests. Return mock data instead.
196          return mockResponse;
197        } else {
198          // Perform callouts for production (non-test) results.
199          Http httpProtocol = new Http();
200          HttpRequest request = new HttpRequest();
201          request.setEndPoint(url);
202          request.setMethod('GET');
203          request.setHeader('Authorization', 'Bearer '+
204              this.connectionInfo.oauthToken);
205          HttpResponse response = httpProtocol.send(request);
206          return response.getBody();
207        }
208    }
209}

DriveDataSourceProvider 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 DriveDataSourceProvider
9    extends DataSource.Provider {
10 
11    /**
12     *   Declares the types of authentication that can be used 
13     *   to access the external system.
14     **/
15    override global List<DataSource.AuthenticationCapability>
16        getAuthenticationCapabilities() {
17        List<DataSource.AuthenticationCapability> capabilities =
18            new List<DataSource.AuthenticationCapability>();
19        capabilities.add(
20            DataSource.AuthenticationCapability.OAUTH);
21        capabilities.add(
22            DataSource.AuthenticationCapability.ANONYMOUS);
23        return capabilities;
24    }
25 
26    /**
27     *   Declares the functional capabilities that the 
28     *   external system supports.
29     **/
30    override global List<DataSource.Capability>
31        getCapabilities() {
32        List<DataSource.Capability> capabilities =
33            new List<DataSource.Capability>();
34        capabilities.add(DataSource.Capability.ROW_QUERY);
35        capabilities.add(DataSource.Capability.SEARCH);
36        return capabilities;
37    }
38
39    /**
40     *   Declares the associated DataSource.Connection class.
41     **/
42    override global DataSource.Connection getConnection(
43        DataSource.ConnectionParams connectionParams) {
44        return new DriveDataSourceConnection(connectionParams);
45    }
46}