After publishing 2 entries talking about
On this one I would like to go one step further on Lightning Connect feature – Apex Connector Framework
As you know, Lightning Connect would allow us to link an external repository to our Salesforce organization. In order to do this connection we can use an existing OData provider or create something by ourselves like we can read on the first Lightning Connect post.
However, since Summer’15 we have the new Apex Connector Framework General Available, so everybody can use it and get advantage of it. At the end we are going to create the connection all in apex so we can forget any other language.
Checa Hotel – Use Case
Let’s back to the same example that we have talked about till now, Checa Hotel one and its reservations.
In order to work with Apex Connector we need to create 2 classes:
HotelReservationConnection
This class must extend DataSource.Connection and although Salesforce documentation shows it as global, you could check on this example that it works properly if we make it public. My advice is to create a global class if we want to expose the code outside of our package, otherwise, it could not be desirable.
public with sharing class HotelReservationConnection extends DataSource.Connection
{
private DataSource.ConnectionParams connectionInfo;
public HotelReservationConnection(DataSource.ConnectionParams connectionParams)
{
this.connectionInfo = connectionParams;
}
}
Now we need to override 3 methods:
Sync
This method will create the new object and its fields. At the end when we click on Sync And Validate button during the creation of the External Data Source, this method would be called.
override public List<DataSource.Table> sync()
{
List<DataSource.Table> tables = new List<DataSource.Table>();
List<DataSource.Column> columns;
columns = new List<DataSource.Column>();
//Standard fields
columns.add(DataSource.Column.url('DisplayUrl'));
columns.add(DataSource.Column.text('ExternalId',255));
//Custom fields
columns.add(DataSource.Column.get('Name', 'Name', 'Name Field for the table', true, true, DataSource.DataType.STRING_SHORT_TYPE, 10, 0));
columns.add(DataSource.Column.get('EndDate', 'End Date', '',true, true, DataSource.DataType.STRING_SHORT_TYPE, 10, 0));
columns.add(DataSource.Column.get('GuestName', 'Guest Name', 'Name of the Guest', true, true, DataSource.DataType.STRING_LONG_TYPE, 80, 0));
columns.add(DataSource.Column.get('Paid', 'Paid', '',true, true, DataSource.DataType.BOOLEAN_TYPE, 255, 0));
columns.add(DataSource.Column.get('Price', 'Price', '',true, true, DataSource.DataType.NUMBER_TYPE, 18, 2));
columns.add(DataSource.Column.get('RoomNumber', 'Room Number', '',true, true, DataSource.DataType.NUMBER_TYPE, 18, 0));
columns.add(DataSource.Column.get('SpecialRequirement', 'Special Requirement', '',true, true, DataSource.DataType.STRING_LONG_TYPE, 255, 0));
columns.add(DataSource.Column.get('StartDate', 'Start Date', '',true, true, DataSource.DataType.STRING_SHORT_TYPE, 10, 0));
//Table creation
DataSource.Table newTable = new DataSource.Table();
newTable.labelSingular = 'New Hotel Reservation';
newTable.labelPlural = 'New Hotel Reservations';
newTable.name = 'NewHotelReservation';
newTable.description = 'Hotel Reservations via Apex Connector';
newTable.nameColumn = 'Name';
newTable.columns = columns;
tables.add(newTable);
return tables;
}
We can highlight 3 blocks
- Standard Fields: We only have to define 2 columns and specify their API Names. These 2 rows are required:
- Display ULR that at end is a link.
- External Id that will help us to set the uniqueness across the whole organization.
- Custom Fields: At this point we are going to specify all fields that we want to show in our object. There are 2 ways to specify every single column:
columns.add(DataSource.Column.text(‘GuestName’,255));
We only specify the API name and its Length.
columns.add(DataSource.Column.get(‘GuestName’,
‘Guest Name’,
‘Name of the Guest’,
true, true,
DataSource.DataType.STRING_LONG_TYPE,
80, 0));
In the case we want to add more information the the field. On above line we can see the API Name, Label, Description, Filtering Disabled field, Sorting Disabled field, Data type and Length.
- Table creation: We will specify some features of the new object. Maybe the one that we are not so use to see is the “name” field. With it we will specify the column that will help us to open every single record instead of having to do it with just the External Id one. Similar as before we can also create the table with a single line, but we would not specify all characteristics as we can do before.
tables.add(DataSource.Table.get(‘NewHotelReservation’,’Name’,columns));
Query
This method will help us to make queries in the organization. It means SOQL and also in the User Interface.
But here we need to specify how can I retrieve and show a single record in the case that we click on the External Id or Name field link (if part of below code) and also how can I show all records if I open the object List View (else piece of code).
override public DataSource.TableResult query(DataSource.QueryContext context)
{
if (context.tableSelection.columnsSelected.size() == 1
&& context.tableSelection.columnsSelected.get(0).aggregation == DataSource.QueryAggregation.COUNT)
{
List<Map<String,Object>> rows = getRows(context);
List<Map<String,Object>> response =
DataSource.QueryUtils.filter(context, getRows(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,response.size());
countResponse.add(countRow);
return DataSource.TableResult.get(context,countResponse);
}
else
{
List<Map<String,Object>> filteredRows = DataSource.QueryUtils.filter(context, getRows(context));
List<Map<String,Object>> sortedRows = DataSource.QueryUtils.sort(context, filteredRows);
List<Map<String,Object>> limitedRows = DataSource.QueryUtils.applyLimitAndOffset(context,sortedRows);
return DataSource.TableResult.get(context, limitedRows);
}
}
Search
Similar as before, this method will help us to make searches that include SOSL as well as global searches throw the User Interface in the organization.
override public List<DataSource.TableResult> search(DataSource.SearchContext context)
{
List<DataSource.TableResult> results = new List<DataSource.TableResult>();
for(DataSource.TableSelection tableSelection : context.tableSelections)
{
results.add(DataSource.TableResult.get(tableSelection,getRows(context)));
}
return results;
}
HotelReservationProvider
Similar as before, this class must extend another, DataSource.Provider and override 3 methods. If you take a look at documentation, this class is also global, but you can see on below example that public is more than enough
public with sharing class HotelReservationProvider extends DataSource.Provider
{
public HotelReservationProvider() {}
}
getAuthenticationCapabilities
This method help us to set up authentication credentials.
override public List<DataSource.AuthenticationCapability> getAuthenticationCapabilities()
{
List<DataSource.AuthenticationCapability> capabilities = new List<DataSource.AuthenticationCapability>();
capabilities.add(DataSource.AuthenticationCapability.ANONYMOUS);
return capabilities;
}
In our case, as the google sheet is public, open to everybody, the authentication is anonymous, but we can determine others like
- Basic
- Certificate
- OAuth
getCapabilities
This second method would allow us to determine what the Datasource is capable during a SOQL or SOSL.
override public 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;
}
Although we can find some others like
- ROW_CREATE
- ROW_UPDATE
- ROW_DELETE
getConnection
Finally the method that creates the connection making a call to our Conector class
override public DataSource.Connection getConnection(DataSource.ConnectionParams connectionParams)
{
return new HotelReservationConnection(connectionParams);
}
How to retrieve data
If you copy-paste above code, it would not compile.
You will find that there is method, getRows, that doesn’t exist. Obviously the name is not the important thing. The most important is the content of the method.
Here is where you are going to define the data, all values you will show in your organization. To highlight, the returned list, a map where the key is the API name of the field that we want to populate and the value is that, a value we will assign to the field.
My advice is to start with some hardcode first, so you can see that all your code is working fine, and then move to read the google sheet.
private List<Map<String,Object>> getRows(DataSource.ReadContext context)
{
List<Map<String, Object>> rows = new List<Map<String, Object>>();
List<HotelReservationGoogleSheetReader.HotelReservation> reservations;
reservations = HotelReservationGoogleSheetReader.readHotelReservations();
Integer counter = 0;
for(HotelReservationGoogleSheetReader.HotelReservation resv : reservations)
{
rows.add(new Map<String,Object>
{
'ExternalId' => resv.ItemId,
'DisplayUrl' => 'http://www.salesforce.com/hotelReservation/2015/',
'Name' => 'NHR-00000' + counter,
'EndDate' => resv.EndDate,
'GuestName' => resv.GuestName,
'Paid' => resv.Paid,
'Price' => resv.Price,
'RoomNumber' => resv.RoomNumber,
'SpecialRequirement' => resv.SpecialRequirement,
'StartDate' => resv.StartDate
});
counter++;
}
}
In my case, I have another class HotelReservationGoogleSheetReader where we will find an inner class HotelReservation with the data of the hotel. And a method readHotelReservations that will use google API to read sheet information and populate HotelReservations lines.
As part of your investigations, I will leave you to find how to read the google sheet, but I can show you a small piece of code. At the end I only make a Http request to get access to google and once I have its cells I will use Salesforce XMLStreamReader to iterate over there.
Apex Connector framework provides lot REST API examples in order to access Google Drive in general so take a look at them.
Http http = new Http();
HttpRequest reqCell = new HttpRequest();
reqCell.setEndpoint(CELL_URL);
reqCell.setMethod('GET');
reqCell.setHeader('content-type', 'application/atom+xml' );
reqCell.setHeader('X-If-No-Redirect', '1');
reqCell.setHeader('Authorization','AuthSub token="'+TOKEN+'"');
HttpResponse resCell = http.send(reqCell);
String bodyCell = resCell.getBody();
XMLStreamReader reader = new XMLStreamReader(bodyCell);
External Data Source creation
Once I have everything, it’s time to create the External Data Source and its External Object
Similar as before go to:
Setup | AppSetup | Develop | External Data Sources
and click on New.
This time, the Type drop down list will include your Provider class. So after selecting it, just click on Validate and Sync button and your new External Object will be created.
On below image you can find some other Providers, as I have more than one in my organization
Summary
This post explain to you what is Apex Connector Framework and how to use it with a simple example.
You can also take a look at my latest session at Dreamforce’15 where I explain step by step how to use Apex Connector Framework – Make Anything a Salesforce Object
Now it is your turn to continue doing some investigations around this topic.