Documente Academic
Documente Profesional
Documente Cultură
Integration Workbook
names and marks. Other marks appearing herein may be trademarks of their respective owners.
Table of Contents
Table of Contents
Force.com Integration Workbook.........................................................................................................2 Before You Begin.................................................................................................................................3 Tutorial #1: Configure Remote Access Settings.....................................................................................5 Tutorial #2: Create a Merchandise Item Using Java................................................................................7
Step 1: Update the Properties File.............................................................................................................................................7 Step 2: Read the Properties File................................................................................................................................................8 Step 3: Log In with OAuth.......................................................................................................................................................8 Step 4: Add Data Via the API..................................................................................................................................................9 Step 5: Compile and Run the App..........................................................................................................................................10 Java Integration Summary.......................................................................................................................................................11
Tutorial #4: Connecting the Warehouse App with an External Service .................................................17
Step 1: Create an External ID Field on Invoice Statement.....................................................................................................17 Step 2: Create a Remote Site Record for the Fulfillment Web Service...................................................................................17 Step 3: Create an @future Method to Post Invoice Statements to the Fulfillment Web Service............................................18 Step 4: Test the @future Method............................................................................................................................................20 Step 5: Create a Trigger to Call the @future Method.............................................................................................................21 Step 6: Test the Integration.....................................................................................................................................................22 Summary.................................................................................................................................................................................23
Intended Audience
This workbook is intended for developers new to the Force.com platform but have existing experience in Java or C#.
Tell Me More....
This workbook is designed so that you can go through the steps as quickly as possible. At the end of some steps, there is an optional Tell Me More section with supporting information. You can find the latest version of this and other workbooks at developer.force.com/workbooks. To learn more about Force.com and to access a rich set of resources, visit Developer Force at http://developer.force.com.
4. To verify that your system can build Java projects, run the appropriate checkinstall program for your configuration: For Windows, double click checkinstall.bat. For OS X, double click checkinstall.command.
5. If the check is successful, youll get a success message, and you can close the window and proceed with the tutorials. If not, a Web browser will open to the Oracle download page for the Java SDK. Follow the online download instructions for your version of Windows. Once youve installed the Java SDK, you can test your installation by re-running the checkinstall program. Note: For most Windows based PCs, you should download the Windows x86 version of the Java SDK.
C# Tutorial Files
1. Download this file: http://bit.ly/aw2012-csharp. 2. Unzip the files to a folder you can find easily. 3. You should see a folder with the following:
4. Verify that your system can compile C# projects by double clicking checkinstall.bat (the file extension may not appear in the folder view). 5. If the check is successful, youll see a command window displaying a success message: SUCCESS: csc.exe file found. You can then close the window and proceed with the tutorials. If unsuccessful, the script will open an Internet Explorer window to the download page for the required .NET Framework. Follow the online download instructions for your version of Windows. Once you have installed the .NET Framework, you can test your installation by re-running checkinstall.bat.
The detail page for your remote access configuration will display a consumer key as well as a consumer secret (which you have to click to reveal). You'll need these in later tutorials.
Tell Me More....
If you're familiar with OAuth, you'll understand that the callback URL is traditionally the URL that a user's browser is redirected to after a successful authentication transaction. For our sample application, however, well use the user/password flow of OAuth. Our application doesnt have a user interface so we cant present a login screen to delegate authentication back to Force.com. The user/password flow is often used for existing on-premise apps, while delegated authentication is more common in mobile application scenarios.
Tell Me More....
The function first loads in the properties file and attempts to read the username and password into variables. If the username or password information in the properties file is invalid, an error exception is thrown. After the function reads all the properties, its ready to log in by calling login2ForceDotCom.
ForceLogin login = login2ForceDotCom(username, passsword, clientId, clientSecret, loginURL);
In the next tutorial well take a look at this function and how OAuth works.
...
The following code snippet shows where the function makes the actual callout to OAuth:
postURL+="/services/oauth2/token?grant_type=password&client_id="+id+"&client_secret="+ secret+"&username="+uName+"&password="+pwd; DefaultHttpClient httpclient = new DefaultHttpClient(); HttpPost post = new HttpPost(postURL); try { HttpResponse response = httpclient.execute(post); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != HttpStatus.SC_OK) { System.out.println("Error authenticating to Force.com:"+statusCode); System.out.println("Error is:"+EntityUtils.toString(response.getEntity())); return null; }
This method creates the URL with the correct parameters, including the consumer public and private keys to correctly identify the application and the users username and password. Normally, an authentication flow with OAuth would use the browser to redirect the user without requiring the application to know the username and password, so that the user only needs to share their credentials with the first party provider of them. In this case were designing an application for your internal use which might be automated later and have no human intervention. Were going to use the password grant type of OAuth in order to authenticate with the username and password. Upon receiving a successful response, the application will need to get the access token from OAuth. This access token provides access to a valid session against the Force.com platform and will allow us to start manipulating data. We also need to know the specific pod the user is using, for example https://na1.salesforce.com versus https://na8.salesforce.com. To get this, the JSON library will parse the result:
String result = EntityUtils.toString(response.getEntity()); JSONObject object = (JSONObject) new JSONTokener(result).nextValue(); ForceLogin login = new AddMerchandise().new ForceLogin(); login.accessToken = object.getString("access_token"); login.instanceUrl = object.getString("instance_url"); return login;
It stores the result as a ForceLogin object for easier access. Now that we have a valid ForceLogin with the access token and the users instance URL, we can start adding in some data.
...
The first step is to create a JSON representation of the data retrieved from the AddMerchandise.properties file:
JSONObject mechandise = new JSONObject()' try { if if if if (name != null && !name.trim().equals("")) mechandise.put("Name", name); (price != null && !price.trim().equals("")) mechandise.put("Price__c", price); (desc != null && !desc.trim().equals("")) mechandise.put("Description__c", desc); (inventory != null && !inventory.trim().equals("")) mechandise.put("Total_Inventory__c", inventory);
We then submit this information to the REST API with the OAuth login information embedded in the header. This allows for a secure transaction of information for the API to work with; and without it, the API would refuse the request.
String restResourceURI = login.instanceUrl + "/services/data/v23.0/sobjects/Merchandise__c/"; HttpPost post = new HttpPost(restResourceURI); StringEntity se = new StringEntity(mechandise.toString()); post.setEntity(se); post.setHeader("Authorization", "OAuth " + login.accessToken); post.setHeader("Content-type", "application/json"); DefaultHttpClient client = new DefaultHttpClient(); HttpResponse resp = client.execute(post); String result = EntityUtils.toString(resp.getEntity()); if (resp.getStatusLine().getStatusCode() != HttpStatus.SC_CREATED){ JSONObject ret = ((JSONArray) new JSONTokener(result).nextValue()).getJSONObject(0); System.out.println("Could not create new Merchandise record:" +resp.getStatusLine().getStatusCode()); System.out.println("Error code:"+ret.getString("errorCode")); System.out.println("Error message:"+ret.getString("message")); return null; } else{ return ((JSONObject) new JSONTokener(result).nextValue()).getString("id"); } } catch (Exception e) { e.printStackTrace(); }
Well then post this to the Merchandise endpoint of the REST API. Notice that even though the Merchandise object is a custom object created through the declarative process, there was no step needed to expose it through the API. Were now ready to compile and run the application.
10
2. For Windows, double-click compileandrun.bat. For OS X, double-click compileandrun.command. Note: Depending on your machine performance, the app may take approximately 30 seconds to compile. If this is the first run, you may see a warning indicating that the classes folder doesnt exist. This folder will be created for you upon first execution. 3. Youll see a Classes Compiled message indicating that your local app has been built successfully, and will now attempt to add the new Merchandise item to the Warehouse app. If any errors exist in the application, theyll be displayed after the Classes Compiled message. Note: If you received any compilation errors, go ahead and debug and fix those now.
4. If your local app has compiled successfully and has connected to Force.com, youll see an error message like the following. Dont worry, this is what we were expecting!
Error code:REQUIRED_FIELD_MISSING Error message:Required fields are missing: [Price__c]
So whats going on with our app? At this point, your application is going to authenticate with Force.com, and attempt to insert your new merchandise item via the REST API. Unfortunately for us, itll fail. Your app failed because the API enforces all the validation rules put in place by the data model on the Force.com platform, regardless of how you connect. The API returns a helpful error message indicating that a required field, Price__c, is missing. Thats right, we didnt enter price into the AddMerchandise.properties file. Lets remedy that now. 1. 2. 3. 4. 5. Open AddMerchandise.properties in a text editor. Where it reads merchandise.price, add 5.99 after the = sign. Save the file. Return to the folder with the Java project files. For Windows, double click run.bat. For OS X, double click run.command.
Now youll receive a message that the item was entered correctly and give you the resulting ID of that item. You can use this unique ID within your application any time you want to work with your new merchandise item. Right now, our sample app only supports inserts, though.
11
12
1. Navigate to where you unzipped the C# tutorial files. 2. Open AddMerchandise.cs in a text editor. 3. Scroll down to the main() function. The main() function acts as the starting point for our application, and is executed when its run from the command line. The main() function calls the constructor AddMerchandise method, which reads the properties we defined in AddMerchandise.txt by running through the lines and parsing the information into an array:
public AddMerchandise() { properties = new Dictionary<string, string>(); foreach (String row in File.ReadAllLines("AddMerchandise.txt")) { properties.Add(row.Split('=')[0], row.Split('=')[1]); } }
The application now has a record of the information you entered into AddMerchandise.txt and is ready to log in to the API using OAuth.
It creates the URL with the correct parameters, including the consumer public and private keys to correctly identify the application and the users username and password. Normally, an authentication flow with OAuth would use the browser to
13
redirect the user without requiring the application to know the username and password, so that the user only needs to share their credentials with the first party provider of them. In this case, though, were designing an application for your internal use which might be automated later and have no human intervention. For this use case, it make sense to use the password grant type of OAuth in order to authenticate with the username and password. Upon getting a successful response, the application will need to get the access token from OAuth. This access token provides access to a valid session against the Force.com platform and allows us to start working with data. We also need to know the specific instance the user is using, for example, https://na1.salesforce.com versus https://na8.salesforce.com.
The first step is to create a JSON representation of the data entered into the AddMerchandise.txt file:
string postData = "{\"Name\" : \"" + properties["merchandise.name"] + "\""; if(properties["merchandise.price"] != "") { postData += ", \"Price__c\" : " + properties["merchandise.price"]; } if(properties["merchandise.inventory"] != "") { postData += ", \"Total_Inventory__c\" : " + properties["merchandise.inventory"]; } if(properties["merchandise.description"] != "") { postData += ", \"Description__c\" : \"" + properties["merchandise.description"] +"\""; } postData += "}";
Well then post this to the Merchandise endpoint of the REST API, using the OAuth information embedded in headers. This allows for the secure transaction of information; and without it, the API would refuse the request. Notice that even though the Merchandise object is a custom object created through the declarative process, there was no step needed to expose it through the API. We can send data just by creating an HTTP POST request, sending it to the endpoint, and then parsing the string for results to show to the user.
string endpoint = "https://" + instance_url + "/services/data/v" + properties["api"] + "/sobjects/Merchandise__c"; string responseFromServer = doHTTPRequest(endpoint, postData, token, true);
14
So whats going on with our app? At this point, your application is going to authenticate with Force.com and attempt to insert your new merchandise item via REST API. Unfortunately for us, itll fail. Your app failed because the API enforces all the validation rules put in place by the data model on the Force.com platform, regardless on how you connect. The API returns a helpful error message indicating a required field, Price__c, is missing. Thats right, we didnt enter price into the AddMerchandise.txt file. Lets remedy that now. 1. 2. 3. 4. 5. Open AddMerchandise.txt in a text editor. Where it reads merchandise.price, add 5.99 after the = sign. Save the file. Return to Windows Explorer and double-click run.bat. Youll see a success message with an ID:
"id":"a0KC000000065pOhMAI" "errors":"" "success":"true"
The message indicates the item was entered correctly and gives you the resulting ID of that item. You can use this unique ID within your application any time you want to work with your new merchandise item (for example, to update the description or price). Right now, our sample app only supports inserts though.
C# Summary
You just created a small application and a properties file to simulate our on-premise buyer application written in C#. This app was able to communicate with the Force.com platform in a secure manner using OAuth for authentication, and standard
15
RESTful calls and JSON to transport data. Using the standard HTTP libraries with Java, it is easy to create integrations between third party systems and the Warehouse app we created on the Force.com platform.
16
Step 2: Create a Remote Site Record for the Fulfillment Web Service
The Force.com platform implements very conservative security controls. By default, calls to external sites are prohibited. Before any Apex callout can call an external site, that site must be registered in the Remote Site Settings page, or the call will fail. Note: Creating a Remote Site is not the same as configuring Remote Access (that we did in Tutorial 1). Remote Access allows authenticated access to Salesforce from external clients, whereas Remote Sites are external sites that Salesforce needs access to. 1. 2. 3. 4. 5. Click Your Name > Setup > Security Controls > Remote Site Settings. Click New Remote Site and fill in the site settings. In the Remote Site Name field, enter FulfillmentWebService (no spaces). In the Remote Site URL field, enter https://fulfillment.herokuapp.com. Leave all other values as they are and click Save.
Now any Apex code in your app will be able to call the fulfillment Web Service.
17
Tell Me More....
If you like, you can skip Step 2 and create and test the callout in Step 3 and Step 4 below to observe the error message that is generated when an app attempts to callout to a URL without permission. Don't forget to come back and add the remote site record, though!
Step 3: Create an @future Method to Post Invoice Statements to the Fulfillment Web Service
Now that your app is allowed to access an external URL, it's time to implement the callout. Apex Triggers are not permitted to make synchronous Web Service calls. This restriction is to ensure a long running Web Service does not hold a lock on a record within your app. In order to achieve our requirements, we'll create an asynchronous method with the @future annotation. When the Trigger calls the asynchronous method, the call will be queued, and execution of the Trigger will complete. Some short time later, the queued method will execute and post the invoice to the order fulfillment Web Service. 1. Go to Apex Classes by clicking Your Name > Setup > Develop > Apex Classes. 2. Click New and paste in the following code. Note: You can also download the code here, http://bit.ly/aw2012-apexint
public class Integration { public class IntegrationException extends Exception { } public class ExternalOrder { public String id {get; set;} public String order_number {get; set;} } // Post an array of invoices to the fulfillment service // This method is designed such that there are only two // queries regardless of the number of invoice IDs passed @future (callout=true) public static void postOrder(List<Id> invoiceIds) { // Get all the line items we'll need List<Line_Item__c> lineItems = [SELECT Name, Merchandise__c, Invoice_Statement__c, Units_Sold__c FROM Line_Item__c WHERE Invoice_Statement__c IN :invoiceIds]; // Now group the line items by invoice Map<Id, Map<String, Line_Item__c>> lineItemsByInvoice = new Map<Id, Map<String, Line_Item__c>>(); for (Line_Item__c lineItem : lineItems) { Map<String, Line_Item__c> lineItemMap = lineItemsByInvoice.get(lineItem.Invoice_Statement__c); if (lineItemMap == null ) { lineItemMap = new Map<String, Line_Item__c>(); lineItemsByInvoice.put(lineItem.Invoice_Statement__c, lineItemMap); } lineItemMap.put(lineItem.Name, lineItem); } // Create the JSON string JSONGenerator gen = JSON.createGenerator(true); gen.writeStartArray(); for (Id invoiceId : invoiceIds) {
18
// Get the invoice name. We'll also need the invoice record // later to set the order ID and update it gen.writeStartObject(); gen.writeStringField('id', invoiceId); // Make a list of the line item names and sort it Map<String, Line_Item__c> lineItemMap = lineItemsByInvoice.get(invoiceId); List<String> lineItemNames = new List<String>(lineItemMap.keySet()); lineItemNames.sort(); // Now we can write the line items gen.writeFieldName('line_items'); gen.writeStartArray(); for ( String name : lineItemNames ) { gen.writeObject(lineItemMap.get(name)); } gen.writeEndArray(); gen.writeEndObject(); } gen.writeEndArray(); String jsonOrders = gen.getAsString(); System.debug('jsonOrders: ' + jsonOrders); // Send the JSON data to the web service HttpRequest req = new HttpRequest(); req.setMethod('POST'); req.setEndpoint('https://fulfillment.herokuapp.com/order'); req.setHeader('Content-Type', 'application/json'); req.setBody(jsonOrders); Http http = new Http(); HTTPResponse res = http.send(req); // Get all the invoices - we'll need to update them List<Invoice_Statement__c> invoices = [SELECT Id FROM Invoice_Statement__c WHERE Id IN :invoiceIds]; // Did it work? if (res.getStatusCode() != 200) { System.debug('Error from ' + req.getEndpoint() + ' : ' + res.getStatusCode() + ' ' + res.getStatus()); // Set all the invoice order IDs to '000000' for ( Invoice_Statement__c invoice : invoices ) { invoice.OrderId__c = '000000'; } } else { // Parse out the external order numbers System.debug('Fulfillment service returned '+res.getBody()); List<ExternalOrder> orders=(List<ExternalOrder>)JSON.deserialize(res.getBody(), List<ExternalOrder>.class); Map<Id, Invoice_Statement__c> invoiceMap = new Map<Id, Invoice_Statement__c>(invoices); // Set the order numbers in the invoices for ( ExternalOrder order : orders ) { Invoice_Statement__c invoice = invoiceMap.get(order.id); invoice.OrderId__c = order.order_number; } } update invoices; } }
Save the file and then examine the above code, you will see that it collects the data it needs to invoke the fulfillment Web Service - an array of invoice objects each containing an array of list items - and generates a JSON-formatted request using the System.JSONGenerator class. After calling the Web Service, the order IDs are parsed out of the response and persisted.
Tell Me More....
Note that the @future method makes only two queries, one for invoices and one for line items, regardless of the number of invoice IDs that are passed in. This pattern, typically called bulkifying makes the code a little more complex, it is a best
19
practice to avoid hitting the database once for each incoming record, therefor making your code much more scalable and efficient.
20
4. Click the box marked Click here to enter Apex Code and enter the following code, with the ID you copied in step 1 in place of the ID in the code.
Integration.postOrder(new List<Id>{'a01E0000000Bs5P'});
5. Click the Execute. You should see two entries appear in the logs. Double click the second line - it should have Future Handler as its operation and a status of Success.
6. Click the Filter checkbox under the Execution Log and type DEBUG as the filter text. Scroll down and double click the last line of the execution log - you should see a popup with the response from the fulfillment Web Service - for example
Now you have a @future method that is able to call the fulfillment Web Service, it's time to tie things together with a Trigger.
21
2. Scroll down to Triggers, click New and paste the following code in place of the trigger skeleton: (You can also download the code here: http://bit.ly/aw2012-ordertrigger )
trigger HandleOrderUpdate on Invoice_Statement__c (after update) { Map<ID, Invoice_Statement__c> oldMap = new Map<ID, Invoice_Statement__c>(Trigger.old); // Make a list of invoice IDs to post, so we can do it in one hit List<Id> invoiceIds = new List<Id>(); for (Invoice_Statement__c invoice: Trigger.new) { // Only post order when status is changing from not closed to closed if (invoice.status__c == 'Closed' && oldMap.get(invoice.Id).status__c != 'Closed'){ invoiceIds.add(invoice.Id); } } if (invoiceIds.size() > 0) { Integration.postOrder(invoiceIds); } }
Notice that the trigger creates a list of IDs for invoices that have been closed in this update - that is, their new status is Closed, but their old status was something other than Closed - and calls the @future method just once, passing the list of IDs. This bulkified pattern is a best practice for triggers you should not assume that there will be only a few updates and make a call to the @future method for each one.
22
Summary
Congratulations! Your app is sending invoices for fulfillment. You have successfully created an asynchronous Apex class which posted invoice details to another app hosted in the cloud. Of course, your external application could reside anywhere as long as you have access via Web Services. Your class used open standards including JSON and REST to transmit data, and a trigger on Invoice Statements to execute the process. Sit back, and celebrate. You have now completed the Integration tutorials. Your warehouse app is now complete!
23