global class yourClass implements Site.UrlRewriter {
global PageReference mapRequestUrl(PageReference
yourFriendlyUrl)
global PageReference[] generateUrlFor(PageReference[]
yourSalesforceUrls);
}
global with sharing class myRewriter implements Site.UrlRewriter {
//Variables to represent the user-friendly URLs for
//account and contact pages
String ACCOUNT_PAGE = '/myaccount/';
String CONTACT_PAGE = '/mycontact/';
//Variables to represent my custom Visualforce pages
//that display account and contact information
String ACCOUNT_VISUALFORCE_PAGE = '/myaccount?id=';
String CONTACT_VISUALFORCE_PAGE = '/mycontact?id=';
global PageReference mapRequestUrl(PageReference
myFriendlyUrl){
String url = myFriendlyUrl.getUrl();
if(url.startsWith(CONTACT_PAGE)){
//Extract the name of the contact from the URL
//For example: /mycontact/Ryan returns Ryan
String name = url.substring(CONTACT_PAGE.length(),
url.length());
//Select the ID of the contact that matches
//the name from the URL
Contact con = [SELECT Id FROM Contact WHERE Name =:
name LIMIT 1];
//Construct a new page reference in the form
//of my Visualforce page
return new PageReference(CONTACT_VISUALFORCE_PAGE + con.id);
}
if(url.startsWith(ACCOUNT_PAGE)){
//Extract the name of the account
String name = url.substring(ACCOUNT_PAGE.length(),
url.length());
//Query for the ID of an account with this name
Account acc = [SELECT Id FROM Account WHERE Name =:name LIMIT 1];
//Return a page in Visualforce format
return new PageReference(ACCOUNT_VISUALFORCE_PAGE + acc.id);
}
//If the URL isn't in the form of a contact or
//account page, continue with the request
return null;
}
global List<PageReference> generateUrlFor(List<PageReference>
mySalesforceUrls){
//A list of pages to return after all the links
//have been evaluated
List<PageReference> myFriendlyUrls = new List<PageReference>();
//a list of all the ids in the urls
List<id> accIds = new List<id>();
// loop through all the urls once, finding all the valid ids
for(PageReference mySalesforceUrl : mySalesforceUrls){
//Get the URL of the page
String url = mySalesforceUrl.getUrl();
//If this looks like an account page, transform it
if(url.startsWith(ACCOUNT_VISUALFORCE_PAGE)){
//Extract the ID from the query parameter
//and store in a list
//for querying later in bulk.
String id= url.substring(ACCOUNT_VISUALFORCE_PAGE.length(),
url.length());
accIds.add(id);
}
}
// Get all the account names in bulk
List <account> accounts = [SELECT Name FROM Account WHERE Id IN :accIds];
// make the new urls
Integer counter = 0;
// it is important to go through all the urls again, so that the order
// of the urls in the list is maintained.
for(PageReference mySalesforceUrl : mySalesforceUrls) {
//Get the URL of the page
String url = mySalesforceUrl.getUrl();
if(url.startsWith(ACCOUNT_VISUALFORCE_PAGE)){
myFriendlyUrls.add(new PageReference(ACCOUNT_PAGE + accounts.get(counter).name));
counter++;
} else {
//If this doesn't start like an account page,
//don't do any transformations
myFriendlyUrls.add(mySalesforceUrl);
}
}
//Return the full list of pages
return myFriendlyUrls;
}
}
重写之前和之后
下面是实现 Apex 类以重写 原始站点 URL。请注意第一张图中基于 ID 的 URL,以及 第二个用户友好的 URL。
如果希望报表快速完成运行,请同步运行报表。否则,我们建议 出于以下原因,您通过 Salesforce API 异步运行报告:
长时间运行的报表在达到超时限制时的风险较低 异步运行。
通过 Apex 的 Salesforce 报告和仪表板 API 可以处理更多数量的 一次异步运行请求。
因为异步运行报表的结果将存储 24 小时 滚动期,它们可用于定期访问。
同步运行报表
运行报表 同步地,使用其中一种方法。例如:ReportManager.runReport()
// Get the report ID
List <Report> reportList = [SELECT Id,DeveloperName FROM Report where
DeveloperName = 'Closed_Sales_This_Quarter'];
String reportId = (String)reportList.get(0).get('Id');
// Run the report
Reports.ReportResults results = Reports.ReportManager.runReport(reportId, true);
System.debug('Synchronous results: ' + results);
异步运行报表
运行报表 异步使用其中一种方法。例如:ReportManager.runAsyncReport()
// Get the report ID
List <Report> reportList = [SELECT Id,DeveloperName FROM Report where
DeveloperName = 'Closed_Sales_This_Quarter'];
String reportId = (String)reportList.get(0).get('Id');
// Run the report
Reports.ReportInstance instance = Reports.ReportManager.runAsyncReport(reportId, true);
System.debug('Asynchronous instance: ' + instance);
// Get the report ID
List <Report> reportList = [SELECT Id,DeveloperName FROM Report where
DeveloperName = 'Closed_Sales_This_Quarter'];
String reportId = (String)reportList.get(0).get('Id');
// Run a report asynchronously
Reports.ReportInstance instance = Reports.ReportManager.runAsyncReport(reportId, true);
System.debug('List of asynchronous runs: ' +
Reports.ReportManager.getReportInstances(reportId));
// Get the report ID
List <Report> reportList = [SELECT Id,DeveloperName FROM Report where
DeveloperName = 'Closed_Sales_This_Quarter'];
String reportId = (String)reportList.get(0).get('Id');
// Run a report synchronously
Reports.reportResults results = Reports.ReportManager.runReport(reportId, true);
// Get the first down-grouping in the report
Reports.Dimension dim = results.getGroupingsDown();
Reports.GroupingValue groupingVal = dim.getGroupings()[0];
System.debug('Key: ' + groupingVal.getKey());
System.debug('Label: ' + groupingVal.getLabel());
System.debug('Value: ' + groupingVal.getValue());
// Construct a fact map key, using the grouping key value
String factMapKey = groupingVal.getKey() + '!T';
// Get the fact map from the report results
Reports.ReportFactWithDetails factDetails =
(Reports.ReportFactWithDetails)results.getFactMap().get(factMapKey);
// Get the first summary amount from the fact map
Reports.SummaryValue sumVal = factDetails.getAggregates()[0];
System.debug('Summary Value: ' + sumVal.getLabel());
// Get the field value from the first data cell of the first row of the report
Reports.ReportDetailRow detailRow = factDetails.getRows()[0];
System.debug(detailRow.getDataCells()[0].getLabel());
筛选报表
要即时获得特定结果,您可以通过 应用程序接口。
通过 API 对筛选器所做的更改不会影响源 报告定义。使用 API,您可以使用最多 20 个自定义字段筛选器进行筛选 并添加筛选器逻辑(例如 AND 和 OR)。但是标准过滤器(如范围), 按行限制进行筛选,并且交叉筛选器不可用。
global class SampleDataSourceConnection
extends DataSource.Connection {
global SampleDataSourceConnection(DataSource.ConnectionParams
connectionParams) {
}
// Add implementation of abstract methods
// ...
这些方法不包含在此代码片段中,但在完整示例中可用 包含在连接类中。通常,过滤器 from 或将用于减少结果 set,但为简单起见,此示例不使用 context 对象。SearchContextQueryContext
// ...
// Helper method to get record values from the external system for the Sample table.
private List<Map<String, Object>> getRows () {
// Get row field values for the Sample table from the external system via a callout.
HttpResponse response = makeGetCallout();
// Parse the JSON response and populate the rows.
Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped(
response.getBody());
Map<String, Object> error = (Map<String, Object>)m.get('error');
if (error != null) {
throwException(string.valueOf(error.get('message')));
}
List<Map<String,Object>> rows = new List<Map<String,Object>>();
List<Object> jsonRows = (List<Object>)m.get('value');
if (jsonRows == null) {
rows.add(foundRow(m));
} else {
for (Object jsonRow : jsonRows) {
Map<String,Object> row = (Map<String,Object>)jsonRow;
rows.add(foundRow(row));
}
}
return rows;
}
// ...
// ...
global override List<DataSource.UpsertResult> upsertRows(DataSource.UpsertContext
context) {
if (context.tableSelected == 'Sample') {
List<DataSource.UpsertResult> results = new List<DataSource.UpsertResult>();
List<Map<String, Object>> rows = context.rows;
for (Map<String, Object> row : rows){
// Make a callout to insert or update records in the external system.
HttpResponse response;
// Determine whether to insert or update a record.
if (row.get('ExternalId') == null){
// Send a POST HTTP request to insert new external record.
// Make an Apex callout and get HttpResponse.
response = makePostCallout(
'{"name":"' + row.get('Name') + '","ExternalId":"' +
row.get('ExternalId') + '"');
}
else {
// Send a PUT HTTP request to update an existing external record.
// Make an Apex callout and get HttpResponse.
response = makePutCallout(
'{"name":"' + row.get('Name') + '","ExternalId":"' +
row.get('ExternalId') + '"',
String.valueOf(row.get('ExternalId')));
}
// Check the returned response.
// Deserialize the response.
Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped(
response.getBody());
if (response.getStatusCode() == 200){
results.add(DataSource.UpsertResult.success(
String.valueOf(m.get('id'))));
}
else {
results.add(DataSource.UpsertResult.failure(
String.valueOf(m.get('id')),
'The callout resulted in an error: ' +
response.getStatusCode()));
}
}
return results;
}
return null;
}
// ...
deleteRows
该方法在以下情况下调用 外部对象记录将被删除。您可以通过以下方式删除外部对象记录 Salesforce 用户界面或 DML。以下示例提供了一个示例 方法的实现。 该示例使用传入的表来确定选择了哪个表,并且仅当名称 所选表为 。删除是 在外部系统中使用每个外部 ID 的标注执行。填充 从标注响应获得的结果。请注意,因为标注是 针对每个 ID 创建,此示例可能会达到 Apex 标注限制。deleteRowsdeleteRowsDeleteContextSampleDataSource.DeleteResult
// ...
global override List<DataSource.DeleteResult> deleteRows(DataSource.DeleteContext
context) {
if (context.tableSelected == 'Sample'){
List<DataSource.DeleteResult> results = new List<DataSource.DeleteResult>();
for (String externalId : context.externalIds){
HttpResponse response = makeDeleteCallout(externalId);
if (response.getStatusCode() == 200){
results.add(DataSource.DeleteResult.success(externalId));
}
else {
results.add(DataSource.DeleteResult.failure(externalId,
'Callout delete error:'
+ response.getBody()));
}
}
return results;
}
return null;
}
// ...
queryMore with the Apex Connector Framework 适用于 Salesforce Connect 的自定义适配器不会自动支持 API 查询中的方法。但是,您的实现必须能够将大型结果集分解为批处理,并使用 SOAP API 中的方法循环访问它们。默认批处理大小为 500 条记录,但查询开发人员可以在查询调用中以编程方式调整该值。queryMorequeryMore
Salesforce Connect 自定义适配器的聚合 如果收到查询,则所选列的属性中具有该值。所选列在 for 的属性中提供。COUNT()QueryAggregation.COUNTaggregationcolumnsSelectedtableSelectionDataSource.QueryContext
override global DataSource.TableResult query(DataSource.QueryContext context) {
// Call out to an external data source and retrieve a set of records.
// We should attempt to get as much information as possible about the
// query from the QueryContext, to minimize the number of records
// that we return.
List<Map<String,Object>> rows = retrieveData(context);
// This only filters the results. Anything in the query that we don’t
// currently support, such as aggregation or sorting, is ignored.
return DataSource.TableResult.get(context, postFilterRecords(
context.tableSelection.filter, rows));
}
private List<Map<String,Object>> retrieveData(DataSource.QueryContext context) {
// Call out to an external data source. Form the callout so that
// it filters as much as possible on the remote site,
// based on the parameters in the QueryContext.
return ...;
}
private List<Map<String,Object>> postFilterRecords(
DataSource.Filter filter, List<Map<String,Object>> rows) {
if (filter == null) {
return rows;
}
DataSource.FilterType type = filter.type;
List<Map<String,Object>> retainedRows = new List<Map<String,Object>>();
if (type == DataSource.FilterType.NOT_) {
// We expect one Filter in the subfilters.
DataSource.Filter subfilter = filter.subfilters.get(0);
for (Map<String,Object> row : rows) {
if (!evaluate(filter, row)) {
retainedRows.add(row);
}
}
return retainedRows;
} else if (type == DataSource.FilterType.AND_) {
// For each filter, find all matches; anything that matches ALL filters
// is returned.
retainedRows = rows;
for (DataSource.Filter subfilter : filter.subfilters) {
retainedRows = postFilterRecords(subfilter, retainedRows);
}
return retainedRows;
} else if (type == DataSource.FilterType.OR_) {
// For each filter, find all matches. Anything that matches
// at least one filter is returned.
for (DataSource.Filter subfilter : filter.subfilters) {
List<Map<String,Object>> matchedRows = postFilterRecords(
subfilter, rows);
retainedRows.addAll(matchedRows);
}
return retainedRows;
} else {
// Find all matches for this filter in our collection of records.
for (Map<String,Object> row : rows) {
if (evaluate(filter, row)) {
retainedRows.add(row);
}
}
return retainedRows;
}
}
private Boolean evaluate(DataSource.Filter filter, Map<String,Object> row) {
if (filter.type == DataSource.FilterType.EQUALS) {
String columnName = filter.columnName;
Object expectedValue = filter.columnValue;
Object foundValue = row.get(columnName);
return expectedValue.equals(foundValue);
} else {
// Throw an exception; implementing other filter types is left
// as an exercise for the reader.
throwException('Unexpected filter type: ' + filter.type);
}
return false;
}
/**
* Extends the DataSource.Connection class to enable
* Salesforce to sync the external system’s schema
* and to handle queries and searches of the external data.
**/
global class DriveDataSourceConnection extends
DataSource.Connection {
private DataSource.ConnectionParams connectionInfo;
/**
* Constructor for DriveDataSourceConnection.
**/
global DriveDataSourceConnection(
DataSource.ConnectionParams connectionInfo) {
this.connectionInfo = connectionInfo;
}
/**
* Called when an external object needs to get a list of
* schema from the external data source, for example when
* the administrator clicks “Validate and Sync” in the
* user interface for the external data source.
**/
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
List<DataSource.Column> columns;
columns = new List<DataSource.Column>();
columns.add(DataSource.Column.text('title', 255));
columns.add(DataSource.Column.text('description',255));
columns.add(DataSource.Column.text('createdDate',255));
columns.add(DataSource.Column.text('modifiedDate',255));
columns.add(DataSource.Column.url('selfLink'));
columns.add(DataSource.Column.url('DisplayUrl'));
columns.add(DataSource.Column.text('ExternalId',255));
tables.add(DataSource.Table.get('googleDrive','title',
columns));
return tables;
}
/**
* Called to query and get results from the external
* system for SOQL queries, list views, and detail pages
* for an external object that’s associated with the
* external data source.
*
* The QueryContext argument represents the query to run
* against a table in the external system.
*
* Returns a list of rows as the query results.
**/
override global DataSource.TableResult query(
DataSource.QueryContext context) {
DataSource.Filter filter = context.tableSelection.filter;
String url;
if (filter != null) {
String thisColumnName = filter.columnName;
if (thisColumnName != null &&
thisColumnName.equals('ExternalId'))
url = 'https://www.googleapis.com/drive/v2/'
+ 'files/' + filter.columnValue;
else
url = 'https://www.googleapis.com/drive/v2/'
+ 'files';
} else {
url = 'https://www.googleapis.com/drive/v2/'
+ 'files';
}
/**
* Filters, sorts, and applies limit and offset clauses.
**/
List<Map<String, Object>> rows =
DataSource.QueryUtils.process(context, getData(url));
return DataSource.TableResult.get(true, null,
context.tableSelection.tableSelected, rows);
}
/**
* Called to do a full text search and get results from
* the external system for SOSL queries and Salesforce
* global searches.
*
* The SearchContext argument represents the query to run
* against a table in the external system.
*
* Returns results for each table that the SearchContext
* requested to be searched.
**/
override global List<DataSource.TableResult> search(
DataSource.SearchContext context) {
List<DataSource.TableResult> results =
new List<DataSource.TableResult>();
for (Integer i =0;i< context.tableSelections.size();i++) {
String entity = context.tableSelections[i].tableSelected;
String url =
'https://www.googleapis.com/drive/v2/files'+
'?q=fullText+contains+\''+context.searchPhrase+'\'';
results.add(DataSource.TableResult.get(
true, null, entity, getData(url)));
}
return results;
}
/**
* Helper method to parse the data.
* The url argument is the URL of the external system.
* Returns a list of rows from the external system.
**/
public List<Map<String, Object>> getData(String url) {
String response = getResponse(url);
List<Map<String, Object>> rows =
new List<Map<String, Object>>();
Map<String, Object> responseBodyMap = (Map<String, Object>)
JSON.deserializeUntyped(response);
/**
* Checks errors.
**/
Map<String, Object> error =
(Map<String, Object>)responseBodyMap.get('error');
if (error!=null) {
List<Object> errorsList =
(List<Object>)error.get('errors');
Map<String, Object> errors =
(Map<String, Object>)errorsList[0];
String errorMessage = (String)errors.get('message');
throw new DataSource.OAuthTokenExpiredException(errorMessage);
}
List<Object> fileItems=(List<Object>)responseBodyMap.get('items');
if (fileItems != null) {
for (Integer i=0; i < fileItems.size(); i++) {
Map<String, Object> item =
(Map<String, Object>)fileItems[i];
rows.add(createRow(item));
}
} else {
rows.add(createRow(responseBodyMap));
}
return rows;
}
/**
* Helper method to populate the External ID and Display
* URL fields on external object records based on the 'id'
* value that’s sent by the external system.
*
* The Map<String, Object> item parameter maps to the data
* that represents a row.
*
* Returns an updated map with the External ID and
* Display URL values.
**/
public Map<String, Object> createRow(
Map<String, Object> item){
Map<String, Object> row = new Map<String, Object>();
for ( String key : item.keySet() ) {
if (key == 'id') {
row.put('ExternalId', item.get(key));
} else if (key=='selfLink') {
row.put(key, item.get(key));
row.put('DisplayUrl', item.get(key));
} else {
row.put(key, item.get(key));
}
}
return row;
}
static String mockResponse = '{' +
' "kind": "drive#file",' +
' "id": "12345",' +
' "selfLink": "files/12345",' +
' "title": "Mock File",' +
' "mimeType": "application/text",' +
' "description": "Mock response that’s used during tests",' +
' "createdDate": "2016-04-20",' +
' "modifiedDate": "2016-04-20",' +
' "version": 1' +
'}';
/**
* Helper method to make the HTTP GET call.
* The url argument is the URL of the external system.
* Returns the response from the external system.
**/
public String getResponse(String url) {
if (System.Test.isRunningTest()) {
// Avoid callouts during tests. Return mock data instead.
return mockResponse;
} else {
// Perform callouts for production (non-test) results.
Http httpProtocol = new Http();
HttpRequest request = new HttpRequest();
request.setEndPoint(url);
request.setMethod('GET');
request.setHeader('Authorization', 'Bearer '+
this.connectionInfo.oauthToken);
HttpResponse response = httpProtocol.send(request);
return response.getBody();
}
}
}
DriveDataSourceProvider Class
/**
* Extends the DataSource.Provider base class to create a
* custom adapter for Salesforce Connect. The class informs
* Salesforce of the functional and authentication
* capabilities that are supported by or required to connect
* to an external system.
**/
global class DriveDataSourceProvider
extends DataSource.Provider {
/**
* Declares the types of authentication that can be used
* to access the external system.
**/
override global List<DataSource.AuthenticationCapability>
getAuthenticationCapabilities() {
List<DataSource.AuthenticationCapability> capabilities =
new List<DataSource.AuthenticationCapability>();
capabilities.add(
DataSource.AuthenticationCapability.OAUTH);
capabilities.add(
DataSource.AuthenticationCapability.ANONYMOUS);
return capabilities;
}
/**
* Declares the functional capabilities that the
* external system supports.
**/
override global List<DataSource.Capability>
getCapabilities() {
List<DataSource.Capability> capabilities =
new List<DataSource.Capability>();
capabilities.add(DataSource.Capability.ROW_QUERY);
capabilities.add(DataSource.Capability.SEARCH);
return capabilities;
}
/**
* Declares the associated DataSource.Connection class.
**/
override global DataSource.Connection getConnection(
DataSource.ConnectionParams connectionParams) {
return new DriveDataSourceConnection(connectionParams);
}
}
适用于 Salesforce Connect 的 Google 图书™自定义适配器
此示例说明如何解决 外部系统的 API:在本例中为 Google 图书 API 系列。为了与 Google 图书™服务集成,我们按如下方式设置 Salesforce Connect。
Google Books API 最多允许返回 40 个结果,因此我们开发了 用于处理超过 40 行的结果集的自定义适配器。
Google Books API 只能按搜索相关性和发布日期进行排序,因此我们 开发我们的自定义适配器以禁用列排序。
/**
* Extends the DataSource.Connection class to enable
* Salesforce to sync the external system metadata
* schema and to handle queries and searches of the external
* data.
**/
global class BooksDataSourceConnection extends
DataSource.Connection {
private DataSource.ConnectionParams connectionInfo;
// Constructor for BooksDataSourceConnection.
global BooksDataSourceConnection(DataSource.ConnectionParams
connectionInfo) {
this.connectionInfo = connectionInfo;
}
/**
* Called when an external object needs to get a list of
* schema from the external data source, for example when
* the administrator clicks “Validate and Sync” in the
* user interface for the external data source.
**/
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
List<DataSource.Column> columns;
columns = new List<DataSource.Column>();
columns.add(getColumn('title'));
columns.add(getColumn('description'));
columns.add(getColumn('publishedDate'));
columns.add(getColumn('publisher'));
columns.add(DataSource.Column.url('DisplayUrl'));
columns.add(DataSource.Column.text('ExternalId', 255));
tables.add(DataSource.Table.get('googleBooks', 'title',
columns));
return tables;
}
/**
* Google Books API v1 doesn't support sorting,
* so we create a column with sortable = false.
**/
private DataSource.Column getColumn(String columnName) {
DataSource.Column column = DataSource.Column.text(columnName,
255);
column.sortable = false;
return column;
}
/**
* Called to query and get results from the external
* system for SOQL queries, list views, and detail pages
* for an external object that's associated with the
* external data source.
*
* The QueryContext argument represents the query to run
* against a table in the external system.
*
* Returns a list of rows as the query results.
**/
override global DataSource.TableResult query(
DataSource.QueryContext contexts) {
DataSource.Filter filter = contexts.tableSelection.filter;
String url;
if (contexts.tableSelection.columnsSelected.size() == 1 &&
contexts.tableSelection.columnsSelected.get(0).aggregation ==
DataSource.QueryAggregation.COUNT) {
return getCount(contexts);
}
if (filter != null) {
String thisColumnName = filter.columnName;
if (thisColumnName != null &&
thisColumnName.equals('ExternalId')) {
url = 'https://www.googleapis.com/books/v1/' +
'volumes?q=' + filter.columnValue +
'&maxResults=1&id=' + filter.columnValue;
return DataSource.TableResult.get(true, null,
contexts.tableSelection.tableSelected,
getData(url));
}
else {
url = 'https://www.googleapis.com/books/' +
'v1/volumes?q=' + filter.columnValue +
'&id=' + filter.columnValue +
'&maxResults=40' + '&startIndex=';
}
} else {
url = 'https://www.googleapis.com/books/v1/' +
'volumes?q=america&' + '&maxResults=40' +
'&startIndex=';
}
/**
* Google Books API v1 supports maxResults of 40
* so we handle pagination explicitly in the else statement
* when we handle more than 40 records per query.
**/
if (contexts.maxResults < 40) {
return DataSource.TableResult.get(true, null,
contexts.tableSelection.tableSelected,
getData(url + contexts.offset));
}
else {
return fetchData(contexts, url);
}
}
/**
* Helper method to fetch results when maxResults is
* greater than 40 (the max value for maxResults supported
* by Google Books API v1).
**/
private DataSource.TableResult fetchData(
DataSource.QueryContext contexts, String url) {
Integer fetchSlot = (contexts.maxResults / 40) + 1;
List<Map<String, Object>> data =
new List<Map<String, Object>>();
Integer startIndex = contexts.offset;
for(Integer count = 0; count < fetchSlot; count++) {
data.addAll(getData(url + startIndex));
if(count == 0)
contexts.offset = 41;
else
contexts.offset += 40;
}
return DataSource.TableResult.get(true, null,
contexts.tableSelection.tableSelected, data);
}
/**
* Helper method to execute count() query.
**/
private DataSource.TableResult getCount(
DataSource.QueryContext contexts) {
String url = 'https://www.googleapis.com/books/v1/' +
'volumes?q=america&projection=full';
List<Map<String,Object>> response =
DataSource.QueryUtils.filter(contexts, getData(url));
List<Map<String, Object>> countResponse =
new List<Map<String, Object>>();
Map<String, Object> countRow =
new Map<String, Object>();
countRow.put(
contexts.tableSelection.columnsSelected.get(0).columnName,
response.size());
countResponse.add(countRow);
return DataSource.TableResult.get(contexts, countResponse);
}
/**
* Called to do a full text search and get results from
* the external system for SOSL queries and Salesforce
* global searches.
*
* The SearchContext argument represents the query to run
* against a table in the external system.
*
* Returns results for each table that the SearchContext
* requested to be searched.
**/
override global List<DataSource.TableResult> search(
DataSource.SearchContext contexts) {
List<DataSource.TableResult> results =
new List<DataSource.TableResult>();
for (Integer i =0; i< contexts.tableSelections.size();i++) {
String entity = contexts.tableSelections[i].tableSelected;
String url = 'https://www.googleapis.com/books/v1' +
'/volumes?q=' + contexts.searchPhrase;
results.add(DataSource.TableResult.get(true, null,
entity,
getData(url)));
}
return results;
}
/**
* Helper method to parse the data.
* Returns a list of rows from the external system.
**/
public List<Map<String, Object>> getData(String url) {
HttpResponse response = getResponse(url);
String body = response.getBody();
List<Map<String, Object>> rows =
new List<Map<String, Object>>();
Map<String, Object> responseBodyMap =
(Map<String, Object>)JSON.deserializeUntyped(body);
/**
* Checks errors.
**/
Map<String, Object> error =
(Map<String, Object>)responseBodyMap.get('error');
if (error!=null) {
List<Object> errorsList =
(List<Object>)error.get('errors');
Map<String, Object> errors =
(Map<String, Object>)errorsList[0];
String messages = (String)errors.get('message');
throw new DataSource.OAuthTokenExpiredException(messages);
}
List<Object> sItems = (List<Object>)responseBodyMap.get('items');
if (sItems != null) {
for (Integer i=0; i< sItems.size(); i++) {
Map<String, Object> item =
(Map<String, Object>)sItems[i];
rows.add(createRow(item));
}
} else {
rows.add(createRow(responseBodyMap));
}
return rows;
}
/**
* Helper method to populate a row based on source data.
*
* The item argument maps to the data that
* represents a row.
*
* Returns an updated map with the External ID and
* Display URL values.
**/
public Map<String, Object> createRow(
Map<String, Object> item) {
Map<String, Object> row = new Map<String, Object>();
for ( String key : item.keySet() ){
if (key == 'id') {
row.put('ExternalId', item.get(key));
} else if (key == 'volumeInfo') {
Map<String, Object> volumeInfoMap =
(Map<String, Object>)item.get(key);
row.put('title', volumeInfoMap.get('title'));
row.put('description',
volumeInfoMap.get('description'));
row.put('DisplayUrl',
volumeInfoMap.get('infoLink'));
row.put('publishedDate',
volumeInfoMap.get('publishedDate'));
row.put('publisher',
volumeInfoMap.get('publisher'));
}
}
return row;
}
/**
* Helper method to make the HTTP GET call.
* The url argument is the URL of the external system.
* Returns the response from the external system.
**/
public HttpResponse getResponse(String url) {
Http httpProtocol = new Http();
HttpRequest request = new HttpRequest();
request.setEndPoint(url);
request.setMethod('GET');
request.setHeader('Authorization', 'Bearer '+
this.connectionInfo.oauthToken);
HttpResponse response = httpProtocol.send(request);
return response;
}
}
BooksDataSourceProvider Class
/**
* Extends the DataSource.Provider base class to create a
* custom adapter for Salesforce Connect. The class informs
* Salesforce of the functional and authentication
* capabilities that are supported by or required to connect
* to an external system.
**/
global class BooksDataSourceProvider extends
DataSource.Provider {
/**
* Declares the types of authentication that can be used
* to access the external system.
**/
override global List<DataSource.AuthenticationCapability>
getAuthenticationCapabilities() {
List<DataSource.AuthenticationCapability> capabilities =
new List<DataSource.AuthenticationCapability>();
capabilities.add(
DataSource.AuthenticationCapability.OAUTH);
capabilities.add(
DataSource.AuthenticationCapability.ANONYMOUS);
return capabilities;
}
/**
* Declares the functional capabilities that the
* external system supports.
**/
override global List<DataSource.Capability>
getCapabilities() {
List<DataSource.Capability> capabilities = new
List<DataSource.Capability>();
capabilities.add(DataSource.Capability.ROW_QUERY);
capabilities.add(DataSource.Capability.SEARCH);
return capabilities;
}
/**
* Declares the associated DataSource.Connection class.
**/
override global DataSource.Connection getConnection(
DataSource.ConnectionParams connectionParams) {
return new BooksDataSourceConnection(connectionParams);
}
}
/**
* Extends the DataSource.Connection class to enable
* Salesforce to sync the external system’s schema
* and to handle queries and searches of the external data.
**/
global class LoopbackDataSourceConnection
extends DataSource.Connection {
/**
* Constructors.
**/
global LoopbackDataSourceConnection(
DataSource.ConnectionParams connectionParams) {
}
global LoopbackDataSourceConnection() {}
/**
* Called when an external object needs to get a list of
* schema from the external data source, for example when
* the administrator clicks “Validate and Sync†in the
* user interface for the external data source.
**/
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
List<DataSource.Column> columns;
columns = new List<DataSource.Column>();
columns.add(DataSource.Column.text('ExternalId', 255));
columns.add(DataSource.Column.url('DisplayUrl'));
columns.add(DataSource.Column.text('Name', 255));
columns.add(
DataSource.Column.number('NumberOfEmployees', 18, 0));
tables.add(
DataSource.Table.get('Looper', 'Name', columns));
return tables;
}
/**
* Called to query and get results from the external
* system for SOQL queries, list views, and detail pages
* for an external object that’s associated with the
* external data source.
*
* The QueryContext argument represents the query to run
* against a table in the external system.
*
* Returns a list of rows as the query results.
**/
override global DataSource.TableResult
query(DataSource.QueryContext context) {
if (context.tableSelection.columnsSelected.size() == 1 &&
context.tableSelection.columnsSelected.get(0).aggregation ==
DataSource.QueryAggregation.COUNT) {
integer count = execCount(getCountQuery(context));
List<Map<String, Object>> countResponse =
new List<Map<String, Object>>();
Map<String, Object> countRow =
new Map<String, Object>();
countRow.put(
context.tableSelection.columnsSelected.get(0).columnName,
count);
countResponse.add(countRow);
return DataSource.TableResult.get(context,countResponse);
} else {
List<Map<String,Object>> rows = execQuery(
getSoqlQuery(context));
return DataSource.TableResult.get(context,rows);
}
}
/**
* Called to do a full text search and get results from
* the external system for SOSL queries and Salesforce
* global searches.
*
* The SearchContext argument represents the query to run
* against a table in the external system.
*
* Returns results for each table that the SearchContext
* requested to be searched.
**/
override global List<DataSource.TableResult>
search(DataSource.SearchContext context) {
return DataSource.SearchUtils.searchByName(context, this);
}
/**
* Helper method to execute the SOQL query and
* return the results.
**/
private List<Map<String,Object>>
execQuery(String soqlQuery) {
List<Account> objs = Database.query(soqlQuery);
List<Map<String,Object>> rows =
new List<Map<String,Object>>();
for (Account obj : objs) {
Map<String,Object> row = new Map<String,Object>();
row.put('Name', obj.Name);
row.put('NumberOfEmployees', obj.NumberOfEmployees);
row.put('ExternalId', obj.Id);
row.put('DisplayUrl',
URL.getOrgDomainUrl().toExternalForm() +
obj.Id);
rows.add(row);
}
return rows;
}
/**
* Helper method to get aggregate count.
**/
private integer execCount(String soqlQuery) {
integer count = Database.countQuery(soqlQuery);
return count;
}
/**
* Helper method to create default aggregate query.
**/
private String getCountQuery(DataSource.QueryContext context) {
String baseQuery = 'SELECT COUNT() FROM Account';
String filter = getSoqlFilter('',
context.tableSelection.filter);
if (filter.length() > 0)
return baseQuery + ' WHERE ' + filter;
return baseQuery;
}
/**
* Helper method to create default query.
**/
private String getSoqlQuery(DataSource.QueryContext context) {
String baseQuery =
'SELECT Id,Name,NumberOfEmployees FROM Account';
String filter = getSoqlFilter('',
context.tableSelection.filter);
if (filter.length() > 0)
return baseQuery + ' WHERE ' + filter;
return baseQuery;
}
/**
* Helper method to handle query filter.
**/
private String getSoqlFilter(String query,
DataSource.Filter filter) {
if (filter == null) {
return query;
}
String append;
DataSource.FilterType type = filter.type;
List<Map<String,Object>> retainedRows =
new List<Map<String,Object>>();
if (type == DataSource.FilterType.NOT_) {
DataSource.Filter subfilter = filter.subfilters.get(0);
append = getSoqlFilter('NOT', subfilter);
} else if (type == DataSource.FilterType.AND_) {
append =
getSoqlFilterCompound('AND', filter.subfilters);
} else if (type == DataSource.FilterType.OR_) {
append =
getSoqlFilterCompound('OR', filter.subfilters);
} else {
append = getSoqlFilterExpression(filter);
}
return query + ' ' + append;
}
/**
* Helper method to handle query subfilters.
**/
private String getSoqlFilterCompound(String operator,
List<DataSource.Filter> subfilters) {
String expression = ' (';
boolean first = true;
for (DataSource.Filter subfilter : subfilters) {
if (first)
first = false;
else
expression += ' ' + operator + ' ';
expression += getSoqlFilter('', subfilter);
}
expression += ') ';
return expression;
}
/**
* Helper method to handle query filter expressions.
**/
private String getSoqlFilterExpression(
DataSource.Filter filter) {
String columnName = filter.columnName;
String operator;
Object expectedValue = filter.columnValue;
if (filter.type == DataSource.FilterType.EQUALS) {
operator = '=';
} else if (filter.type ==
DataSource.FilterType.NOT_EQUALS) {
operator = '<>';
} else if (filter.type ==
DataSource.FilterType.LESS_THAN) {
operator = '<';
} else if (filter.type ==
DataSource.FilterType.GREATER_THAN) {
operator = '>';
} else if (filter.type ==
DataSource.FilterType.LESS_THAN_OR_EQUAL_TO) {
operator = '<=';
} else if (filter.type ==
DataSource.FilterType.GREATER_THAN_OR_EQUAL_TO) {
operator = '>=';
} else if (filter.type ==
DataSource.FilterType.STARTS_WITH) {
return mapColumnName(columnName) +
' LIKE \'' + String.valueOf(expectedValue) + '%\'';
} else if (filter.type ==
DataSource.FilterType.ENDS_WITH) {
return mapColumnName(columnName) +
' LIKE \'%' + String.valueOf(expectedValue) + '\'';
} else if (filter.type ==
DataSource.FilterType.LIKE_) {
return mapColumnName(columnName) +
' LIKE \'' + String.valueOf(expectedValue) + '\'';
} else {
throwException(
'Implementing other filter types is left as an exercise for the reader: '
+ filter.type);
}
return mapColumnName(columnName) +
' ' + operator + ' ' + wrapValue(expectedValue);
}
/**
* Helper method to map column names.
**/
private String mapColumnName(String apexName) {
if (apexName.equalsIgnoreCase('ExternalId'))
return 'Id';
if (apexName.equalsIgnoreCase('DisplayUrl'))
return 'Id';
return apexName;
}
/**
* Helper method to wrap expression Strings with quotes.
**/
private String wrapValue(Object foundValue) {
if (foundValue instanceof String)
return '\'' + String.valueOf(foundValue) + '\'';
return String.valueOf(foundValue);
}
}
LoopbackDataSourceProvider Class
/**
* Extends the DataSource.Provider base class to create a
* custom adapter for Salesforce Connect. The class informs
* Salesforce of the functional and authentication
* capabilities that are supported by or required to connect
* to an external system.
**/
global class LoopbackDataSourceProvider
extends DataSource.Provider {
/**
* Declares the types of authentication that can be used
* to access the external system.
**/
override global List<DataSource.AuthenticationCapability>
getAuthenticationCapabilities() {
List<DataSource.AuthenticationCapability> capabilities =
new List<DataSource.AuthenticationCapability>();
capabilities.add(
DataSource.AuthenticationCapability.ANONYMOUS);
capabilities.add(
DataSource.AuthenticationCapability.BASIC);
return capabilities;
}
/**
* Declares the functional capabilities that the
* external system supports.
**/
override global List<DataSource.Capability>
getCapabilities() {
List<DataSource.Capability> capabilities =
new List<DataSource.Capability>();
capabilities.add(DataSource.Capability.ROW_QUERY);
capabilities.add(DataSource.Capability.SEARCH);
return capabilities;
}
/**
* Declares the associated DataSource.Connection class.
**/
override global DataSource.Connection
getConnection(DataSource.ConnectionParams connectionParams) {
return new LoopbackDataSourceConnection();
}
}
/**
* Defines the connection to GitHub REST API v3 to support
* querying of GitHub profiles.
* Extends the DataSource.Connection class to enable
* Salesforce to sync the external system’s schema
* and to handle queries and searches of the external data.
**/
global class GitHubDataSourceConnection extends
DataSource.Connection {
private DataSource.ConnectionParams connectionInfo;
/**
* Constructor for GitHubDataSourceConnection
**/
global GitHubDataSourceConnection(
DataSource.ConnectionParams connectionInfo) {
this.connectionInfo = connectionInfo;
}
/**
* Called to query and get results from the external
* system for SOQL queries, list views, and detail pages
* for an external object that’s associated with the
* external data source.
*
* The queryContext argument represents the query to run
* against a table in the external system.
*
* Returns a list of rows as the query results.
**/
override global DataSource.TableResult query(
DataSource.QueryContext context) {
DataSource.Filter filter = context.tableSelection.filter;
String url;
if (filter != null) {
String thisColumnName = filter.columnName;
if (thisColumnName != null &&
(thisColumnName.equals('ExternalId') ||
thisColumnName.equals('login')))
url = 'https://api.github.com/users/'
+ filter.columnValue;
else
url = 'https://api.github.com/users';
} else {
url = 'https://api.github.com/users';
}
/**
* Filters, sorts, and applies limit and offset clauses.
**/
List<Map<String, Object>> rows =
DataSource.QueryUtils.process(context, getData(url));
return DataSource.TableResult.get(true, null,
context.tableSelection.tableSelected, rows);
}
/**
* Defines the schema for the external system.
* Called when the administrator clicks “Validate and Sync”
* in the user interface for the external data source.
**/
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
List<DataSource.Column> columns;
columns = new List<DataSource.Column>();
// Defines the indirect lookup field. (For this to work,
// make sure your Contact standard object has a
// custom unique, external ID field called github_username.)
columns.add(DataSource.Column.indirectLookup(
'login', 'Contact', 'github_username__c'));
columns.add(DataSource.Column.text('id', 255));
columns.add(DataSource.Column.text('name',255));
columns.add(DataSource.Column.text('company',255));
columns.add(DataSource.Column.text('bio',255));
columns.add(DataSource.Column.text('followers',255));
columns.add(DataSource.Column.text('following',255));
columns.add(DataSource.Column.url('html_url'));
columns.add(DataSource.Column.url('DisplayUrl'));
columns.add(DataSource.Column.text('ExternalId',255));
tables.add(DataSource.Table.get('githubProfile','login',
columns));
return tables;
}
/**
* Called to do a full text search and get results from
* the external system for SOSL queries and Salesforce
* global searches.
*
* The SearchContext argument represents the query to run
* against a table in the external system.
*
* Returns results for each table that the SearchContext
* requested to be searched.
**/
override global List<DataSource.TableResult> search(
DataSource.SearchContext context) {
List<DataSource.TableResult> results =
new List<DataSource.TableResult>();
for (Integer i =0;i< context.tableSelections.size();i++) {
String entity = context.tableSelections[i].tableSelected;
// Search usernames
String url = 'https://api.github.com/users/'
+ context.searchPhrase;
results.add(DataSource.TableResult.get(
true, null, entity, getData(url)));
}
return results;
}
/**
* Helper method to parse the data.
* The url argument is the URL of the external system.
* Returns a list of rows from the external system.
**/
public List<Map<String, Object>> getData(String url) {
String response = getResponse(url);
// Standardize response string
if (!response.contains('"items":')) {
if (response.substring(0,1).equals('{')) {
response = '[' + response + ']';
}
response = '{"items": ' + response + '}';
}
List<Map<String, Object>> rows =
new List<Map<String, Object>>();
Map<String, Object> responseBodyMap = (Map<String, Object>)
JSON.deserializeUntyped(response);
/**
* Checks errors.
**/
Map<String, Object> error =
(Map<String, Object>)responseBodyMap.get('error');
if (error!=null) {
List<Object> errorsList =
(List<Object>)error.get('errors');
Map<String, Object> errors =
(Map<String, Object>)errorsList[0];
String errorMessage = (String)errors.get('message');
throw new
DataSource.OAuthTokenExpiredException(errorMessage);
}
List<Object> fileItems =
(List<Object>)responseBodyMap.get('items');
if (fileItems != null) {
for (Integer i=0; i < fileItems.size(); i++) {
Map<String, Object> item =
(Map<String, Object>)fileItems[i];
rows.add(createRow(item));
}
} else {
rows.add(createRow(responseBodyMap));
}
return rows;
}
/**
* Helper method to populate the External ID and Display
* URL fields on external object records based on the 'id'
* value that’s sent by the external system.
*
* The Map<String, Object> item parameter maps to the data
* that represents a row.
*
* Returns an updated map with the External ID and
* Display URL values.
**/
public Map<String, Object> createRow(
Map<String, Object> item){
Map<String, Object> row = new Map<String, Object>();
for ( String key : item.keySet() ) {
if (key == 'login') {
row.put('ExternalId', item.get(key));
} else if (key=='html_url') {
row.put('DisplayUrl', item.get(key));
}
row.put(key, item.get(key));
}
return row;
}
/**
* Helper method to make the HTTP GET call.
* The url argument is the URL of the external system.
* Returns the response from the external system.
**/
public String getResponse(String url) {
// Perform callouts for production (non-test) results.
Http httpProtocol = new Http();
HttpRequest request = new HttpRequest();
request.setEndPoint(url);
request.setMethod('GET');
HttpResponse response = httpProtocol.send(request);
return response.getBody();
}
}
GitHubDataSourceProvider 类
/**
* Extends the DataSource.Provider base class to create a
* custom adapter for Salesforce Connect. The class informs
* Salesforce of the functional and authentication
* capabilities that are supported by or required to connect
* to an external system.
**/
global class GitHubDataSourceProvider
extends DataSource.Provider {
/**
* For simplicity, this example declares that the external
* system doesn’t require authentication by returning
* AuthenticationCapability.ANONYMOUS as the sole entry
* in the list of authentication capabilities.
**/
override global List<DataSource.AuthenticationCapability>
getAuthenticationCapabilities() {
List<DataSource.AuthenticationCapability> capabilities =
new List<DataSource.AuthenticationCapability>();
capabilities.add(
DataSource.AuthenticationCapability.ANONYMOUS);
return capabilities;
}
/**
* Declares the functional capabilities that the
* external system supports, in this case
* only SOQL queries.
**/
override global List<DataSource.Capability>
getCapabilities() {
List<DataSource.Capability> capabilities =
new List<DataSource.Capability>();
capabilities.add(DataSource.Capability.ROW_QUERY);
return capabilities;
}
/**
* Declares the associated DataSource.Connection class.
**/
override global DataSource.Connection getConnection(
DataSource.ConnectionParams connectionParams) {
return new GitHubDataSourceConnection(connectionParams);
}
}
/**
* Defines the connection to Stack Exchange API v2.2 to support
* querying of Stack Overflow users (stackoverflowUser)
* and posts (stackoverflowPost).
* Extends the DataSource.Connection class to enable
* Salesforce to sync the external system’s schema
* and to handle queries of the external data.
**/
global class StackOverflowDataSourceConnection extends
DataSource.Connection {
private DataSource.ConnectionParams connectionInfo;
/**
* Constructor for StackOverflowDataSourceConnection
**/
global StackOverflowDataSourceConnection(
DataSource.ConnectionParams connectionInfo) {
this.connectionInfo = connectionInfo;
}
/**
* Defines the schema for the external system.
* Called when the administrator clicks “Validate and Sync”
* in the user interface for the external data source.
**/
override global List<DataSource.Table> sync() {
List<DataSource.Table> tables =
new List<DataSource.Table>();
// Defines columns for the table of Stack OverFlow posts
List<DataSource.Column> postColumns =
new List<DataSource.Column>();
// Defines the external lookup field.
postColumns.add(DataSource.Column.externalLookup(
'owner_id', 'stackoverflowUser__x'));
postColumns.add(DataSource.Column.text('title', 255));
postColumns.add(DataSource.Column.text('view_count', 255));
postColumns.add(DataSource.Column.text('question_id',255));
postColumns.add(DataSource.Column.text('creation_date',255));
postColumns.add(DataSource.Column.text('score',255));
postColumns.add(DataSource.Column.url('link'));
postColumns.add(DataSource.Column.url('DisplayUrl'));
postColumns.add(DataSource.Column.text('ExternalId',255));
tables.add(DataSource.Table.get('stackoverflowPost','title',
postColumns));
// Defines columns for the table of Stack OverFlow users
List<DataSource.Column> userColumns =
new List<DataSource.Column>();
userColumns.add(DataSource.Column.text('user_id', 255));
userColumns.add(DataSource.Column.text('display_name', 255));
userColumns.add(DataSource.Column.text('location',255));
userColumns.add(DataSource.Column.text('creation_date',255));
userColumns.add(DataSource.Column.url('website_url',255));
userColumns.add(DataSource.Column.text('reputation',255));
userColumns.add(DataSource.Column.url('link'));
userColumns.add(DataSource.Column.url('DisplayUrl'));
userColumns.add(DataSource.Column.text('ExternalId',255));
tables.add(DataSource.Table.get('stackoverflowUser',
'Display_name', userColumns));
return tables;
}
/**
* Called to query and get results from the external
* system for SOQL queries, list views, and detail pages
* for an external object that’s associated with the
* external data source.
*
* The QueryContext argument represents the query to run
* against a table in the external system.
*
* Returns a list of rows as the query results.
**/
override global DataSource.TableResult query(
DataSource.QueryContext context) {
DataSource.Filter filter = context.tableSelection.filter;
String url;
// Sets the URL to query Stack Overflow posts
if (context.tableSelection.tableSelected
.equals('stackoverflowPost')) {
if (filter != null) {
String thisColumnName = filter.columnName;
if (thisColumnName != null &&
thisColumnName.equals('ExternalId'))
url = 'https://api.stackexchange.com/2.2/'
+ 'questions/' + filter.columnValue
+ '?order=desc&sort=activity'
+ '&site=stackoverflow';
else
url = 'https://api.stackexchange.com/2.2/'
+ 'questions'
+ '?order=desc&sort=activity'
+ '&site=stackoverflow';
} else {
url = 'https://api.stackexchange.com/2.2/'
+ 'questions'
+ '?order=desc&sort=activity'
+ '&site=stackoverflow';
}
// Sets the URL to query Stack Overflow users
} else if (context.tableSelection.tableSelected
.equals('stackoverflowUser')) {
if (filter != null) {
String thisColumnName = filter.columnName;
if (thisColumnName != null &&
thisColumnName.equals('ExternalId'))
url = 'https://api.stackexchange.com/2.2/'
+ 'users/' + filter.columnValue
+ '?order=desc&sort=reputation'
+ '&site=stackoverflow';
else
url = 'https://api.stackexchange.com/2.2/'
+ 'users' +
'?order=desc&sort=reputation&site=stackoverflow';
} else {
url = 'https://api.stackexchange.com/2.2/'
+ 'users' + '?order=desc&sort=reputation'
+ '&site=stackoverflow';
}
}
/**
* Filters, sorts, and applies limit and offset clauses.
**/
List<Map<String, Object>> rows =
DataSource.QueryUtils.process(context, getData(url));
return DataSource.TableResult.get(true, null,
context.tableSelection.tableSelected, rows);
}
/**
* Helper method to parse the data.
* The url argument is the URL of the external system.
* Returns a list of rows from the external system.
**/
public List<Map<String, Object>> getData(String url) {
String response = getResponse(url);
List<Map<String, Object>> rows =
new List<Map<String, Object>>();
Map<String, Object> responseBodyMap = (Map<String, Object>)
JSON.deserializeUntyped(response);
/**
* Checks errors.
**/
Map<String, Object> error =
(Map<String, Object>)responseBodyMap.get('error');
if (error!=null) {
List<Object> errorsList =
(List<Object>)error.get('errors');
Map<String, Object> errors =
(Map<String, Object>)errorsList[0];
String errorMessage = (String)errors.get('message');
throw new
DataSource.OAuthTokenExpiredException(errorMessage);
}
List<Object> fileItems=
(List<Object>)responseBodyMap.get('items');
if (fileItems != null) {
for (Integer i=0; i < fileItems.size(); i++) {
Map<String, Object> item =
(Map<String, Object>)fileItems[i];
rows.add(createRow(item));
}
} else {
rows.add(createRow(responseBodyMap));
}
return rows;
}
/**
* Helper method to populate the External ID and Display
* URL fields on external object records based on the 'id'
* value that’s sent by the external system.
*
* The Map<String, Object> item parameter maps to the data
* that represents a row.
*
* Returns an updated map with the External ID and
* Display URL values.
**/
public Map<String, Object> createRow(
Map<String, Object> item) {
Map<String, Object> row = new Map<String, Object>();
for ( String key : item.keySet() ) {
if (key.equals('question_id') || key.equals('user_id')) {
row.put('ExternalId', item.get(key));
} else if (key.equals('link')) {
row.put('DisplayUrl', item.get(key));
} else if (key.equals('owner')) {
Map<String, Object> ownerMap =
(Map<String, Object>)item.get(key);
row.put('owner_id', ownerMap.get('user_id'));
}
row.put(key, item.get(key));
}
return row;
}
/**
* Helper method to make the HTTP GET call.
* The url argument is the URL of the external system.
* Returns the response from the external system.
**/
public String getResponse(String url) {
// Perform callouts for production (non-test) results.
Http httpProtocol = new Http();
HttpRequest request = new HttpRequest();
request.setEndPoint(url);
request.setMethod('GET');
HttpResponse response = httpProtocol.send(request);
return response.getBody();
}
}
StackOverflowPostDataSourceProvider Class
/**
* Extends the DataSource.Provider base class to create a
* custom adapter for Salesforce Connect. The class informs
* Salesforce of the functional and authentication
* capabilities that are supported by or required to connect
* to an external system.
**/
global class StackOverflowPostDataSourceProvider
extends DataSource.Provider {
/**
* For simplicity, this example declares that the external
* system doesn’t require authentication by returning
* AuthenticationCapability.ANONYMOUS as the sole entry
* in the list of authentication capabilities.
**/
override global List<DataSource.AuthenticationCapability>
getAuthenticationCapabilities() {
List<DataSource.AuthenticationCapability> capabilities =
new List<DataSource.AuthenticationCapability>();
capabilities.add(
DataSource.AuthenticationCapability.ANONYMOUS);
return capabilities;
}
/**
* Declares the functional capabilities that the
* external system supports, in this case
* only SOQL queries.
**/
override global List<DataSource.Capability>
getCapabilities() {
List<DataSource.Capability> capabilities =
new List<DataSource.Capability>();
capabilities.add(DataSource.Capability.ROW_QUERY);
return capabilities;
}
/**
* Declares the associated DataSource.Connection class.
**/
override global DataSource.Connection getConnection(
DataSource.ConnectionParams connectionParams) {
return new
StackOverflowDataSourceConnection(connectionParams);
}
}
// Identify the article to promote in search results
List<MyArticle__kav> articles = [SELECT Id FROM MyArticle__kav WHERE PublishStatus='Online' AND Language='en_US' AND Id='Article Id'];
// Define the promotion rule
SearchPromotionRule s = new SearchPromotionRule(
Query='Salesforce',
PromotedEntity=articles[0]);
// Save the new rule
insert s;
public class IRMController {
private String downloadEndpoint;
public IRMController() {
downloadEndpoint = '';
}
public void applyIrmControl() {
String versionId = ApexPages.currentPage().getParameters().get('id');
Http h = new Http();
//Instantiate a new HTTP request, specify the method (GET) as well as the endpoint
HttpRequest req = new HttpRequest();
req.setEndpoint('http://irmsystem?versionId=' + versionId);
req.setMethod('GET');
// Send the request, and retrieve a response
HttpResponse r = h.send(req);
JSONParser parser = JSON.createParser(r.getBody());
while (parser.nextToken() != null) {
if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&
(parser.getText() == 'downloadEndpoint')) {
parser.nextToken();
downloadEndpoint = parser.getText();
break;
}
}
}
public String getDownloadEndpoint() {
return downloadEndpoint;
}
}
例
下面的示例创建一个实现接口的类,并返回 阻止将文件下载到移动设备的下载处理程序 装置。
ContentDownloadHandlerFactory
// Allow customization of the content Download experience
public class ContentDownloadHandlerFactoryImpl implements Sfc.ContentDownloadHandlerFactory {
public Sfc.ContentDownloadHandler getContentDownloadHandler(List<ID> ids, Sfc.ContentDownloadContext context) {
Sfc.ContentDownloadHandler contentDownloadHandler = new Sfc.ContentDownloadHandler();
if(context == Sfc.ContentDownloadContext.MOBILE) {
contentDownloadHandler.isDownloadAllowed = false;
contentDownloadHandler.downloadErrorMessage = 'Downloading a file from a mobile device isn't allowed.';
return contentDownloadHandler;
}
contentDownloadHandler.isDownloadAllowed = true;
return contentDownloadHandler;
}
例
您还可以阻止从移动设备下载文件,并要求 文件必须通过 IRM 控制。
// Allow customization of the content Download experience
public class ContentDownloadHandlerFactoryImpl implements Sfc.ContentDownloadHandlerFactory {
public Sfc.ContentDownloadHandler getContentDownloadHandler(List<ID> ids, Sfc.ContentDownloadContext context) {
Sfc.ContentDownloadHandler contentDownloadHandler = new Sfc.ContentDownloadHandler();
if(UserInfo.getUserId() == '005xx000001SvogAAC') {
contentDownloadHandler.isDownloadAllowed = true;
return contentDownloadHandler;
}
if(context == Sfc.ContentDownloadContext.MOBILE) {
contentDownloadHandler.isDownloadAllowed = false;
contentDownloadHandler.downloadErrorMessage = 'Downloading a file from a mobile device isn't allowed.';
return contentDownloadHandler;
}
contentDownloadHandler.isDownloadAllowed = false;
contentDownloadHandler.downloadErrorMessage = 'This file needs to be IRM controlled. You're not allowed to download it';
contentDownloadHandler.redirectUrl ='/apex/IRMControl?Id='+id.get(0);
return contentDownloadHandler;
}
}
// Add a value to the cache
DateTime dt = DateTime.parse('06/16/2015 11:46 AM');
Cache.Session.put('ns1.partition1.orderDate', dt);
if (Cache.Session.contains('ns1.partition1.orderDate')) {
DateTime cachedDt = (DateTime)Cache.Session.get('ns1.partition1.orderDate');
}
若要引用调用类的默认分区和命名空间,请省略前缀并指定键名称。namespace.partition
Cache.Session.put('orderDate', dt);
if (Cache.Session.contains('orderDate')) {
DateTime cachedDt = (DateTime)Cache.Session.get('orderDate');
}
// Get a cached value
Object obj = Cache.Session.get('ns1.partition1.orderDate');
// Cast return value to a specific data type
DateTime dt2 = (DateTime)obj;
// Get partition
Cache.SessionPartition sessionPart = Cache.Session.getPartition('myNs.myPartition');
// Retrieve cache value from the partition
if (sessionPart.contains('BookTitle')) {
String cachedTitle = (String)sessionPart.get('BookTitle');
}
// Add cache value to the partition
sessionPart.put('OrderDate', Date.today());
此示例在分区上调用该方法 一个表达式,而不将分区实例分配给变量。get
// Or use dot notation to call partition methods
String cachedAuthor = (String)Cache.Session.getPartition('myNs.myPartition').get('BookAuthor');
// Add a value to the cache
DateTime dt = DateTime.parse('06/16/2015 11:46 AM');
Cache.Org.put('ns1.partition1.orderDate', dt);
if (Cache.Org.contains('ns1.partition1.orderDate')) {
DateTime cachedDt = (DateTime)Cache.Org.get('ns1.partition1.orderDate');
}
若要引用调用类的默认分区和命名空间,请省略前缀并指定键名称。namespace.partition
Cache.Org.put('orderDate', dt);
if (Cache.Org.contains('orderDate')) {
DateTime cachedDt = (DateTime)Cache.Org.get('orderDate');
}
// Get a cached value
Object obj = Cache.Org.get('ns1.partition1.orderDate');
// Cast return value to a specific data type
DateTime dt2 = (DateTime)obj;
// Get partition
Cache.OrgPartition orgPart = Cache.Org.getPartition('myNs.myPartition');
// Retrieve cache value from the partition
if (orgPart.contains('BookTitle')) {
String cachedTitle = (String)orgPart.get('BookTitle');
}
// Add cache value to the partition
orgPart.put('OrderDate', Date.today());
此示例在分区上调用该方法 一个表达式,而不将分区实例分配给变量。get
// Or use dot notation to call partition methods
String cachedAuthor = (String)Cache.Org.getPartition('myNs.myPartition').get('BookAuthor');
在控制器类中,创建一个实现接口并重写该方法的内部类。然后将 SOQL 代码添加到方法中,并将用户 ID 作为其 参数。CacheBuilderdoLoad(String var)doLoad(String var)
class UserInfoCache implements Cache.CacheBuilder {
public Object doLoad(String userid) {
User u = (User)[SELECT Id, IsActive, username FROM User WHERE id =: userid];
return u;
}
}
// Don't do this!
public class MyController {
public void initCache() {
List<Account> accts = [SELECT Id, Name, Phone, Industry, Description FROM
Account limit 1000];
for (Integer i=0; i<accts.size(); i++) {
Cache.Org.put('acct' + i, accts.get(i));
}
}
}
相反,将数据包装在几个相当大的项目中,而不会超过 单个缓存项的大小。
// Do this instead.
public class MyController {
public void initCache() {
List<Account> accts = [SELECT Id, Name, Phone, Industry, Description FROM
Account limit 1000];
Cache.Org.put('accts', accts);
}
}
该方法强制立即计算 聚合指定权限集组的权限。由于强制计算很重要 针对 Apex CPU 限制,并且可能需要复杂的数据设置,最佳做法是尽量减少 执行此操作的次数。calculatePermissionSetGroup()
将此测试设置为在“测试设置方法”中运行一次,然后在后续测试中重复使用数据。
@isTest public class PSGTest {
@isTest static void testPSG() {
// get the PSG by name (may have been modified in deployment)
PermissionSetGroup psg = [select Id, Status from PermissionSetGroup where DeveloperName='MyPSG'];
// force calculation of the PSG if it is not already Updated
if (psg.Status != 'Updated') {
Test.calculatePermissionSetGroup(psg.Id);
}
// assign PSG to current user (this fails if PSG is Outdated)
insert new PermissionSetAssignment(PermissionSetGroupId = psg.Id, AssigneeId = UserInfo.getUserId());
// additional tests to validate permissions granted by PSG
}
}
// DeployCallbackContext subclass for testing that returns myJobId
public class TestingDeployCallbackContext extends Metadata.DeployCallbackContext {
private myJobId = null; // define to a canned ID you can use for testing
public override Id getCallbackJobId() {
return myJobId;
}
}
该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。
实现接口的类必须调用这些方法。Process.Plugin
名字
参数
返回类型
描述
describe
Process.PluginDescribeResult
返回描述这一点的对象 方法调用。Process.PluginDescribeResult
invoke
Process.PluginRequest
Process.PluginResult
当实现 接口被实例化。
示例实现
global class flowChat implements Process.Plugin {
// The main method to be implemented. The Flow calls this at runtime.
global Process.PluginResult invoke(Process.PluginRequest request) {
// Get the subject of the Chatter post from the flow
String subject = (String) request.inputParameters.get('subject');
// Use the Chatter APIs to post it to the current user's feed
FeedItem fItem = new FeedItem();
fItem.ParentId = UserInfo.getUserId();
fItem.Body = 'Flow Update: ' + subject;
insert fItem;
// return to Flow
Map<String,Object> result = new Map<String,Object>();
return new Process.PluginResult(result);
}
// Returns the describe information for the interface
global Process.PluginDescribeResult describe() {
Process.PluginDescribeResult result = new Process.PluginDescribeResult();
result.Name = 'flowchatplugin';
result.Tag = 'chat';
result.inputParameters = new
List<Process.PluginDescribeResult.InputParameter>{
new Process.PluginDescribeResult.InputParameter('subject',
Process.PluginDescribeResult.ParameterType.STRING, true)
};
result.outputParameters = new
List<Process.PluginDescribeResult.OutputParameter>{ };
return result;
}
}
测试类
以下是上述类的测试类。
@isTest
private class flowChatTest {
static testmethod void flowChatTests() {
flowChat plugin = new flowChat();
Map<String,Object> inputParams = new Map<String,Object>();
string feedSubject = 'Flow is alive';
InputParams.put('subject', feedSubject);
Process.PluginRequest request = new Process.PluginRequest(inputParams);
plugin.invoke(request);
}
}
使用 Process.PluginRequest 类
该类传递输入 实现流接口的类中的参数。
Process.PluginRequest
提示
我们建议使用注解而不是接口。@InvocableMethodProcess.Plugin
该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。
此类没有方法。构造 函数 签名:
Process.PluginRequest (Map<String,Object>)
下面是使用一个输入参数实例化类的示例。Process.PluginRequest
Map<String,Object> inputParams = new Map<String,Object>();
string feedSubject = 'Flow is alive';
InputParams.put('subject', feedSubject);
Process.PluginRequest request = new Process.PluginRequest(inputParams);
代码示例
在此示例中,代码从流程中返回 Chatter 帖子的主题,并发布 它到当前用户的 饲料。
global Process.PluginResult invoke(Process.PluginRequest request) {
// Get the subject of the Chatter post from the flow
String subject = (String) request.inputParameters.get('subject');
// Use the Chatter APIs to post it to the current user's feed
FeedPost fpost = new FeedPost();
fpost.ParentId = UserInfo.getUserId();
fpost.Body = 'Flow Update: ' + subject;
insert fpost;
// return to Flow
Map<String,Object> result = new Map<String,Object>();
return new Process.PluginResult(result);
}
// describes the interface
global Process.PluginDescribeResult describe() {
Process.PluginDescribeResult result = new Process.PluginDescribeResult();
result.inputParameters = new List<Process.PluginDescribeResult.InputParameter>{
new Process.PluginDescribeResult.InputParameter('subject',
Process.PluginDescribeResult.ParameterType.STRING, true)
};
result.outputParameters = new List<Process.PluginDescribeResult.OutputParameter>{ };
return result;
}
}
使用 Process.PluginResult 类
该类返回 从实现接口的类输出参数到 流。
Process.PluginResult
提示
我们建议使用注解而不是接口。@InvocableMethodProcess.Plugin
该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。
您可以使用以下格式之一实例化类:
Process.PluginResult
Process.PluginResult (Map<String,Object>)
Process.PluginResult (String, Object)
当您有多个结果或不知道有多少个结果时,请使用地图 将返回结果。下面是实例化类的示例。
Process.PluginResult
string url = 'https://docs.google.com/document/edit?id=abc';
String status = 'Success';
Map<String,Object> result = new Map<String,Object>();
result.put('url', url);
result.put('status',status);
new Process.PluginResult(result);
该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。
该类没有 支持以下功能。
Process.PluginDescribeResult
查询
数据修改
电子邮件
顶点嵌套标注
Process.PluginDescribeResult 类和 子类属性
下面是该类的构造函数。
Process.PluginDescribeResult
Process.PluginDescribeResult classname = new Process.PluginDescribeResult();
PluginDescribeResult 类属性
PluginDescribeResult.InputParameter 类属性
PluginDescribeResult.OutputParameter 类属性
下面是该类的构造函数。
Process.PluginDescribeResult.InputParameter
Process.PluginDescribeResult.InputParameter ip = new
Process.PluginDescribeResult.InputParameter(Name,Optional_description_string,
Process.PluginDescribeResult.ParameterType.Enum, Boolean_required);
下面是该类的构造函数。
Process.PluginDescribeResult.OutputParameter
Process.PluginDescribeResult.OutputParameter op = new
new Process.PluginDescribeResult.OutputParameter(Name,Optional description string,
Process.PluginDescribeResult.ParameterType.Enum);
Process.PluginDescribeResult.inputParameters =
new List<Process.PluginDescribeResult.InputParameter>{
new Process.PluginDescribeResult.InputParameter(Name,Optional_description_string,
Process.PluginDescribeResult.ParameterType.Enum, Boolean_required)
为 例:
Process.PluginDescribeResult result = new Process.PluginDescribeResult();
result.setDescription('this plugin gets the name of a user');
result.setTag ('userinfo');
result.inputParameters = new List<Process.PluginDescribeResult.InputParameter>{
new Process.PluginDescribeResult.InputParameter('FullName',
Process.PluginDescribeResult.ParameterType.STRING, true),
new Process.PluginDescribeResult.InputParameter('DOB',
Process.PluginDescribeResult.ParameterType.DATE, true),
};
Process.PluginDescribeResult.outputParameters = new List<Process.PluginDescribeResult.OutputParameter>{
new Process.PluginDescribeResult.OutputParameter(Name,Optional description string,
Process.PluginDescribeResult.ParameterType.Enum)
为 例:
Process.PluginDescribeResult result = new Process.PluginDescribeResult();
result.setDescription('this plugin gets the name of a user');
result.setTag ('userinfo');
result.outputParameters = new List<Process.PluginDescribeResult.OutputParameter>{
new Process.PluginDescribeResult.OutputParameter('URL',
Process.PluginDescribeResult.ParameterType.STRING),
两个类都采用枚举。有效值为:
Process.PluginDescribeResult.ParameterType
布尔
日期
日期时间
十进制
双
浮
编号
整数
长
字符串
为 例:
Process.PluginDescribeResult result = new Process.PluginDescribeResult();
result.outputParameters = new List<Process.PluginDescribeResult.OutputParameter>{
new Process.PluginDescribeResult.OutputParameter('URL',
Process.PluginDescribeResult.ParameterType.STRING, true),
new Process.PluginDescribeResult.OutputParameter('STATUS',
Process.PluginDescribeResult.ParameterType.STRING),
};
Process.Plugin 数据类型 转换
了解如何在 Apex 和返回给 .例如,流中的文本数据会转换 在 Apex 中字符串数据。
Process.Plugin
提示
我们建议使用注解而不是接口。@InvocableMethodProcess.Plugin
该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。
该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。
// Converts a lead as an action in a flow.
global class VWFConvertLead implements Process.Plugin {
// This method runs when called by a flow's legacy Apex action.
global Process.PluginResult invoke(
Process.PluginRequest request) {
// Set up variables to store input parameters from
// the flow.
String leadID = (String) request.inputParameters.get(
'LeadID');
String contactID = (String)
request.inputParameters.get('ContactID');
String accountID = (String)
request.inputParameters.get('AccountID');
String convertedStatus = (String)
request.inputParameters.get('ConvertedStatus');
Boolean overWriteLeadSource = (Boolean)
request.inputParameters.get('OverwriteLeadSource');
Boolean createOpportunity = (Boolean)
request.inputParameters.get('CreateOpportunity');
String opportunityName = (String)
request.inputParameters.get('ContactID');
Boolean sendEmailToOwner = (Boolean)
request.inputParameters.get('SendEmailToOwner');
// Set the default handling for booleans.
if (overWriteLeadSource == null)
overWriteLeadSource = false;
if (createOpportunity == null)
createOpportunity = true;
if (sendEmailToOwner == null)
sendEmailToOwner = false;
// Convert the lead by passing it to a helper method.
Map<String,Object> result = new Map<String,Object>();
result = convertLead(leadID, contactID, accountID,
convertedStatus, overWriteLeadSource,
createOpportunity, opportunityName,
sendEmailToOwner);
return new Process.PluginResult(result);
}
// This method describes the plug-in and its inputs from
// and outputs to the flow.
// Implementing this method makes the class available
// in Flow Builder as a legacy Apex action.
global Process.PluginDescribeResult describe() {
// Set up plugin metadata
Process.PluginDescribeResult result = new
Process.PluginDescribeResult();
result.description =
'The LeadConvert Flow Plug-in converts a lead into ' +
'an account, a contact, and ' +
'(optionally)an opportunity.';
result.tag = 'Lead Management';
// Create a list that stores both mandatory and optional
// input parameters from the flow.
// NOTE: Only primitive types (STRING, NUMBER, etc.) are
// supported. Collections aren't supported.
result.inputParameters = new
List<Process.PluginDescribeResult.InputParameter>{
// Lead ID (mandatory)
new Process.PluginDescribeResult.InputParameter(
'LeadID',
Process.PluginDescribeResult.ParameterType.STRING,
true),
// Account Id (optional)
new Process.PluginDescribeResult.InputParameter(
'AccountID',
Process.PluginDescribeResult.ParameterType.STRING,
false),
// Contact ID (optional)
new Process.PluginDescribeResult.InputParameter(
'ContactID',
Process.PluginDescribeResult.ParameterType.STRING,
false),
// Status to use once converted
new Process.PluginDescribeResult.InputParameter(
'ConvertedStatus',
Process.PluginDescribeResult.ParameterType.STRING,
true),
new Process.PluginDescribeResult.InputParameter(
'OpportunityName',
Process.PluginDescribeResult.ParameterType.STRING,
false),
new Process.PluginDescribeResult.InputParameter(
'OverwriteLeadSource',
Process.PluginDescribeResult.ParameterType.BOOLEAN,
false),
new Process.PluginDescribeResult.InputParameter(
'CreateOpportunity',
Process.PluginDescribeResult.ParameterType.BOOLEAN,
false),
new Process.PluginDescribeResult.InputParameter(
'SendEmailToOwner',
Process.PluginDescribeResult.ParameterType.BOOLEAN,
false)
};
// Create a list that stores output parameters sent
// to the flow.
result.outputParameters = new List<
Process.PluginDescribeResult.OutputParameter>{
// Account ID of the converted lead
new Process.PluginDescribeResult.OutputParameter(
'AccountID',
Process.PluginDescribeResult.ParameterType.STRING),
// Contact ID of the converted lead
new Process.PluginDescribeResult.OutputParameter(
'ContactID',
Process.PluginDescribeResult.ParameterType.STRING),
// Opportunity ID of the converted lead
new Process.PluginDescribeResult.OutputParameter(
'OpportunityID',
Process.PluginDescribeResult.ParameterType.STRING)
};
return result;
}
/**
* Implementation of the LeadConvert plug-in.
* Converts a given lead with several options:
* leadID - ID of the lead to convert
* contactID -
* accountID - ID of the Account to attach the converted
* Lead/Contact/Opportunity to.
* convertedStatus -
* overWriteLeadSource -
* createOpportunity - true if you want to create a new
* Opportunity upon conversion
* opportunityName - Name of the new Opportunity.
* sendEmailtoOwner - true if you are changing owners upon
* conversion and want to notify the new Opportunity owner.
*
* returns: a Map with the following output:
* AccountID - ID of the Account created or attached
* to upon conversion.
* ContactID - ID of the Contact created or attached
* to upon conversion.
* OpportunityID - ID of the Opportunity created
* upon conversion.
*/
public Map<String,String> convertLead (
String leadID,
String contactID,
String accountID,
String convertedStatus,
Boolean overWriteLeadSource,
Boolean createOpportunity,
String opportunityName,
Boolean sendEmailToOwner
) {
Map<String,String> result = new Map<String,String>();
if (leadId == null) throw new ConvertLeadPluginException(
'Lead Id cannot be null');
// check for multiple leads with the same ID
Lead[] leads = [Select Id, FirstName, LastName, Company
From Lead where Id = :leadID];
if (leads.size() > 0) {
Lead l = leads[0];
// CheckAccount = true, checkContact = false
if (accountID == null && l.Company != null) {
Account[] accounts = [Select Id, Name FROM Account
where Name = :l.Company LIMIT 1];
if (accounts.size() > 0) {
accountId = accounts[0].id;
}
}
// Perform the lead conversion.
Database.LeadConvert lc = new Database.LeadConvert();
lc.setLeadId(leadID);
lc.setOverwriteLeadSource(overWriteLeadSource);
lc.setDoNotCreateOpportunity(!createOpportunity);
lc.setConvertedStatus(convertedStatus);
if (sendEmailToOwner != null) lc.setSendNotificationEmail(
sendEmailToOwner);
if (accountId != null && accountId.length() > 0)
lc.setAccountId(accountId);
if (contactId != null && contactId.length() > 0)
lc.setContactId(contactId);
if (createOpportunity) {
lc.setOpportunityName(opportunityName);
}
Database.LeadConvertResult lcr = Database.convertLead(
lc, true);
if (lcr.isSuccess()) {
result.put('AccountID', lcr.getAccountId());
result.put('ContactID', lcr.getContactId());
if (createOpportunity) {
result.put('OpportunityID',
lcr.getOpportunityId());
}
} else {
String error = lcr.getErrors()[0].getMessage();
throw new ConvertLeadPluginException(error);
}
} else {
throw new ConvertLeadPluginException(
'No leads found with Id : "' + leadId + '"');
}
return result;
}
// Utility exception class
class ConvertLeadPluginException extends Exception {}
}
// Test class for the lead convert Apex plug-in.
@isTest
private class VWFConvertLeadTest {
static testMethod void basicTest() {
// Create test lead
Lead testLead = new Lead(
Company='Test Lead',FirstName='John',LastName='Doe');
insert testLead;
LeadStatus convertStatus =
[Select Id, MasterLabel from LeadStatus
where IsConverted=true limit 1];
// Create test conversion
VWFConvertLead aLeadPlugin = new VWFConvertLead();
Map<String,Object> inputParams = new Map<String,Object>();
Map<String,Object> outputParams = new Map<String,Object>();
inputParams.put('LeadID',testLead.ID);
inputParams.put('ConvertedStatus',
convertStatus.MasterLabel);
Process.PluginRequest request = new
Process.PluginRequest(inputParams);
Process.PluginResult result;
result = aLeadPlugin.invoke(request);
Lead aLead = [select name, id, isConverted
from Lead where id = :testLead.ID];
System.Assert(aLead.isConverted);
}
/*
* This tests lead conversion with
* the Account ID specified.
*/
static testMethod void basicTestwithAccount() {
// Create test lead
Lead testLead = new Lead(
Company='Test Lead',FirstName='John',LastName='Doe');
insert testLead;
Account testAccount = new Account(name='Test Account');
insert testAccount;
// System.debug('ACCOUNT BEFORE' + testAccount.ID);
LeadStatus convertStatus = [Select Id, MasterLabel
from LeadStatus where IsConverted=true limit 1];
// Create test conversion
VWFConvertLead aLeadPlugin = new VWFConvertLead();
Map<String,Object> inputParams = new Map<String,Object>();
Map<String,Object> outputParams = new Map<String,Object>();
inputParams.put('LeadID',testLead.ID);
inputParams.put('AccountID',testAccount.ID);
inputParams.put('ConvertedStatus',
convertStatus.MasterLabel);
Process.PluginRequest request = new
Process.PluginRequest(inputParams);
Process.PluginResult result;
result = aLeadPlugin.invoke(request);
Lead aLead =
[select name, id, isConverted, convertedAccountID
from Lead where id = :testLead.ID];
System.Assert(aLead.isConverted);
//System.debug('ACCOUNT AFTER' + aLead.convertedAccountID);
System.AssertEquals(testAccount.ID, aLead.convertedAccountID);
}
/*
* This tests lead conversion with the Account ID specified.
*/
static testMethod void basicTestwithAccounts() {
// Create test lead
Lead testLead = new Lead(
Company='Test Lead',FirstName='John',LastName='Doe');
insert testLead;
Account testAccount1 = new Account(name='Test Lead');
insert testAccount1;
Account testAccount2 = new Account(name='Test Lead');
insert testAccount2;
// System.debug('ACCOUNT BEFORE' + testAccount.ID);
LeadStatus convertStatus = [Select Id, MasterLabel
from LeadStatus where IsConverted=true limit 1];
// Create test conversion
VWFConvertLead aLeadPlugin = new VWFConvertLead();
Map<String,Object> inputParams = new Map<String,Object>();
Map<String,Object> outputParams = new Map<String,Object>();
inputParams.put('LeadID',testLead.ID);
inputParams.put('ConvertedStatus',
convertStatus.MasterLabel);
Process.PluginRequest request = new
Process.PluginRequest(inputParams);
Process.PluginResult result;
result = aLeadPlugin.invoke(request);
Lead aLead =
[select name, id, isConverted, convertedAccountID
from Lead where id = :testLead.ID];
System.Assert(aLead.isConverted);
}
/*
* -ve Test
*/
static testMethod void errorTest() {
// Create test lead
// Lead testLead = new Lead(Company='Test Lead',
// FirstName='John',LastName='Doe');
LeadStatus convertStatus = [Select Id, MasterLabel
from LeadStatus where IsConverted=true limit 1];
// Create test conversion
VWFConvertLead aLeadPlugin = new VWFConvertLead();
Map<String,Object> inputParams = new Map<String,Object>();
Map<String,Object> outputParams = new Map<String,Object>();
inputParams.put('LeadID','00Q7XXXXxxxxxxx');
inputParams.put('ConvertedStatus',convertStatus.MasterLabel);
Process.PluginRequest request = new
Process.PluginRequest(inputParams);
Process.PluginResult result;
try {
result = aLeadPlugin.invoke(request);
}
catch (Exception e) {
System.debug('EXCEPTION' + e);
System.AssertEquals(1,1);
}
}
/*
* This tests the describe() method
*/
static testMethod void describeTest() {
VWFConvertLead aLeadPlugin =
new VWFConvertLead();
Process.PluginDescribeResult result =
aLeadPlugin.describe();
System.AssertEquals(
result.inputParameters.size(), 8);
System.AssertEquals(
result.OutputParameters.size(), 3);
}
}
使用该方法发送的单封电子邮件计入发送组织的每日单封电子邮件限制。当此限制为 到达时,对方法 using 的调用将被拒绝,并且用户会收到错误代码。然而 允许通过应用程序发送单封电子邮件。sendEmailsendEmailSingleEmailMessageSINGLE_EMAIL_LIMIT_EXCEEDED
使用该方法发送的大量电子邮件 计入发送组织的每日群发电子邮件限制。当达到此限制时, 对方法 using 的调用被拒绝,并且用户会收到错误代码。sendEmailsendEmailMassEmailMessageMASS_MAIL_LIMIT_EXCEEDED
// First, reserve email capacity for the current Apex transaction to ensure
// that we won't exceed our daily email limits when sending email after
// the current transaction is committed.
Messaging.reserveSingleEmailCapacity(2);
// Processes and actions involved in the Apex transaction occur next,
// which conclude with sending a single email.
// Now create a new single email message object
// that will send out a single email to the addresses in the To, CC & BCC list.
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
// Strings to hold the email addresses to which you are sending the email.
String[] toAddresses = new String[] {'user@acme.com'};
String[] ccAddresses = new String[] {'smith@gmail.com'};
// Assign the addresses for the To and CC lists to the mail object.
mail.setToAddresses(toAddresses);
mail.setCcAddresses(ccAddresses);
// Specify the address used when the recipients reply to the email.
mail.setReplyTo('support@acme.com');
// Specify the name used as the display name.
mail.setSenderDisplayName('Salesforce Support');
// Specify the subject line for your email address.
mail.setSubject('New Case Created : ' + case.Id);
// Set to True if you want to BCC yourself on the email.
mail.setBccSender(false);
// Optionally append the Salesforce email signature to the email.
// The email address of the user executing the Apex Code will be used.
mail.setUseSignature(false);
// Specify the text content of the email.
mail.setPlainTextBody('Your Case: ' + case.Id +' has been created.');
mail.setHtmlBody('Your case:<b> ' + case.Id +' </b>has been created.<p>'+
'To view your case <a href=https://MyDomainName.my.salesforce.com/'+case.Id+'>click here.</a>');
// Send the email you have created.
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
trigger ReviewFeedItem on FeedItem (before insert) {
for (Integer i = 0; i<trigger.new.size(); i++) {
// We don't want to leak "test phrase" information.
if (trigger.new[i].body.containsIgnoreCase('test phrase')) {
trigger.new[i].status = 'PendingReview';
System.debug('caught one for pendingReview');
}
}
}