Thursday, October 10, 2019

External Data Service

As mentioned in my previous blog : https://chintanblog.blogspot.com/2017/05/odataheroku-with-salesforce-integrate.html  , if we can expose the external data as OData 2.0 or 4.0, we can directly consume them in salesforce as External Objects. The second option would be to directly consume those web service using External Data Service and consume them as external objects.



As you can see above, either you can put thin layer against external web service to do protocol conversion to support Odata (as mentioned in https://chintanblog.blogspot.com/2017/05/odataheroku-with-salesforce-integrate.html) or write the plugin in Apex.

Here we will discuss how to write External Data Service in Apex.

1) I created a sample external web service to just for the demo - It is HR service, which return list of employees, and also employees by name, account, etc..

https://github.com/c-shah/basic-authentication






2) As next step, we need to write

ExternalDataSourceProvider -> ExternalDataSourceConnection -> ExternalDataService

a) ExternalDataSourceProvider

this class basically extends DataSource.Provider, and provides what capability are supported from both authentication and database support (e.g. query, update)



/**
 * Created by chint on 10/4/2019.
 */

global without sharing class ExternalDataSourceProvider extends DataSource.Provider {

    override global List<DataSource.AuthenticationCapability> getAuthenticationCapabilities() {
        System.debug(' ExternalDataSourceProvider.getAuthenticationCapabilities ');
        List<DataSource.AuthenticationCapability> capabilities =  new List<DataSource.AuthenticationCapability> {
                DataSource.AuthenticationCapability.ANONYMOUS
        };
        System.debug(' ExternalDataSourceProvider.getAuthenticationCapabilities ' + JSON.serialize(capabilities) );
        return capabilities;
    }

    override global List<DataSource.Capability> getCapabilities() {
        System.debug(' ExternalDataSourceProvider.getCapabilities ');
        List<DataSource.Capability> capabilities = new List<DataSource.Capability> {
            DataSource.Capability.ROW_QUERY
        };
        System.debug(' ExternalDataSourceProvider.getCapabilities ' + JSON.serialize(capabilities) );
        return capabilities;
    }

    override global DataSource.Connection getConnection(DataSource.ConnectionParams connectionParams) {
        System.debug(' ExternalDataSourceProvider.getConnection connectionParams: ' + connectionParams);
        DataSource.Connection connection = new ExternalDataSourceConnection(connectionParams);
        System.debug(' ExternalDataSourceProvider.getConnection connection: ' + connection);
        return connection;
    }

}


b) ExternalDataSourceConnection

This class extends DataSource.Connection, and responsible for providing table structures and as well as data results which will be consumed by query associated with External Objects



/**
 * Created by chint on 10/4/2019.
 */

global without sharing class ExternalDataSourceConnection extends DataSource.Connection {

    private DataSource.ConnectionParams connectionInfo ;

    global ExternalDataSourceConnection(DataSource.ConnectionParams connectionInfo) {
        System.debug(' ExternalDataSourceConnection.ExternalDataSourceConnection connectionInfo: ' + JSON.serialize(connectionInfo));
        this.connectionInfo = connectionInfo;
    }

    override global List<DataSource.Table> sync() {
        System.debug(' ExternalDataSourceConnection.sync ');
        List<DataSource.Table> tables = ExternalDataService.getInstance().getTables();
        System.debug(' ExternalDataSourceConnection.sync tables ' + JSON.serialize(tables) );
        return tables;
    }

    override global DataSource.TableResult query(DataSource.QueryContext context) {
        try {
            printContent(context);
            DataSource.TableResult tableResult = DataSource.TableResult.get(context, ExternalDataService.getInstance().getData(context) );
            System.debug(' ExternalDataSourceConnection.query tableResult ' + JSON.serialize(tableResult));
            return tableResult;
        } catch (Exception currentException) {
            String message = currentException.getMessage() + currentException.getStackTraceString() ;
            System.debug(' ExternalDataSourceConnection.query exception : ' + message );
            throw new DataSource.DataSourceException(message);
        }
    }

    public static void printContent(DataSource.QueryContext context) {
        System.debug(' ExternalDataSourceConnection.printContent queryMoreToken ' + context.queryMoreToken + ' tableSelected ' + context.tableSelection.tableSelected );
        DataSource.Filter filter = context.tableSelection.filter;
        List<DataSource.Order> orders = context.tableSelection.order;
        List<DataSource.ColumnSelection> columnsSelected = context.tableSelection.columnsSelected;
        if( filter != null ) {
            System.debug('ExternalDataSourceConnection.printContent filter columnName ' + filter.columnName + ' columnValue ' + filter.columnValue + ' subfilters ' + filter.subfilters + ' tableName ' + filter.tableName + ' type ' + filter.type );
        }
        if( orders != null ) {
            for(DataSource.Order order : orders ) {
                System.debug('ExternalDataSourceConnection.printContent order columnName ' + order.columnName  + ' tableName ' + order.tableName + ' direction ' + order.direction );
            }
        }
        if( columnsSelected != null ) {
            for(DataSource.ColumnSelection columnSelected : columnsSelected ) {
                System.debug('ExternalDataSourceConnection.printContent columnSelected columnName ' + columnSelected.columnName  + ' tableName ' + columnSelected.tableName + ' aggregation ' + columnSelected.aggregation );
            }
        }
    }
}



3) ExternalDataService, which takes care of calling web service and returning the data needed for ExternalDataSourceConnection.


/**
 * Created by chint on 10/4/2019.
 */

public without sharing class ExternalDataService {

    private static ExternalDataService instance;

    private ExternalDataService() {
        System.debug(' ExternalDataService.ExternalDataService ');
    }

    public static ExternalDataService getInstance() {
        System.debug(' ExternalDataService.getInstance ');
        if( instance == null ) {
            instance = new ExternalDataService();
        }
        return instance;
    }

    public List<DataSource.Table> getTables() {
        System.debug(' ExternalDataService.getTables ');
        List<DataSource.Table> tables = new List<DataSource.Table> {
                getEmployeeTable(),
                getAddressTable() };
        System.debug(' ExternalDataService.getTables tables ' + JSON.serialize(tables) );
        return tables;
    }

    public List<Map<String, Object>> getData(DataSource.QueryContext context) {
        System.debug(' ExternalDataService.getData context ' + context );
        return getEmployeeData(context);
    }

    private DataSource.Table getEmployeeTable() {
        List<DataSource.Column> columns;
        columns = new List<DataSource.Column>();
        columns.add(DataSource.Column.text('ExternalId', 255));
        columns.add(DataSource.Column.text('Name', 255));
        columns.add(DataSource.Column.url('DisplayUrl'));
        columns.add(DataSource.Column.indirectLookup('EmployeeAccountKey', 'Account', 'Account_Key__c'));
        return DataSource.Table.get('Employee', 'ExternalId', columns);
    }

    private List<Map<String, Object>> getDummyData() {
        Map<String,Object> row1 = new Map<String,Object> {
                'ExternalId' => 'Emp1',
                'Name' => 'Chintan Shah',
                'EmployeeAccountKey' => 'ACN1'
        };
        Map<String,Object> row2 = new Map<String,Object> {
                'ExternalId' => 'Emp2',
                'Name' => 'Mark Twain',
                'EmployeeAccountKey' => 'ACN1'
        };
        Map<String,Object> row3 = new Map<String,Object> {
                'ExternalId' => 'Emp3',
                'Name' => 'John Doe',
                'EmployeeAccountKey' => 'ACN2'
        };
        List<Map<String,Object>> dataRows = new List<Map<String,Object>> { row1, row2, row3 };
        return dataRows;
    }

    private List<Map<String, Object>> getEmployeeData(DataSource.QueryContext context) {
        if( context.tableSelection != null && context.tableSelection.tableSelected == 'Employee' ) {
            DataSource.Filter filter = context.tableSelection.filter;
            String url = '/hr/employees';
            if( filter != null ) {
                url = '/hr/employee/' + filter.columnName + '/' + filter.columnValue;
            }
            List<Map<String,Object>> response = httpGetCallout('HerokuBasicAuth', url);
            return response;
        }
        return new List<Map<String,Object>>();
    }

    public static List<Map<String,Object>> httpGetCallout(String namedCredentials, String url) {
        List<Map<String,Object>> returnEmployees = new List<Map<String,Object>>();
        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint('callout:' + namedCredentials + url );
        request.setMethod('GET');
        HttpResponse response = http.send(request);
        if (response.getStatusCode() == 200) {
            System.debug(' response.getBody() ' + response.getBody() );
            List<Object> employees = (List<Object>) JSON.deserializeUntyped( response.getBody() );
            for(Object employee : employees ) {
                Map<String, Object> currentEmployee = ( Map<String, Object> ) ( employee );
                returnEmployees.add( currentEmployee );
            }
        }
        system.debug( ' ExternalDataService.httpGetCallout returnEmployees ' + returnEmployees  );
        return returnEmployees;
    }

    private DataSource.Table getAddressTable() {
        DataSource.Table table = new DataSource.Table();
        List<DataSource.Column> columns;
        columns = new List<DataSource.Column>();
        columns.add(DataSource.Column.text('ExternalId', 255));
        columns.add(DataSource.Column.text('Street', 255));
        columns.add(DataSource.Column.text('ZipCode', 255));
        columns.add(DataSource.Column.url('DisplayUrl'));
        columns.add(DataSource.Column.indirectLookup('AddressAccountKey', 'Account', 'Account_Key__c'));
        return DataSource.Table.get('Address', 'ExternalId', columns);
    }

    private List<Map<String, Object>> getAddressData(DataSource.QueryContext context) {
        List<Map<String,Object>> dataRows = new List<Map<String,Object>>();
        return dataRows;
    }


}

This basically :
  • understand the context (name of the table, query criteria
  • calls the webservice 
  • returns the data in List> format. 

We can see external Objects now:







Once it is done, it is ready to be tested on Account page layout, due to indirect lookup relationship, Employee external object would be available in Account Page layout.






This way we can consume external service as external object without external Odata layer.


Source Code : 

https://github.com/springsoa/springsoa-salesforce/tree/master/classes
https://github.com/c-shah/basic-authentication

1 comment:

yash sharma said...

Best Data Recovery Solution for recover lost or deleted data from MAC Data, Hard drive, Mobile Data, RAID Server, Email Data, ssd/flash Media storage Recovery in delhi
Data recovery services