Syntax Highlighter JS

Friday, June 29, 2012

Downloading Visual Force pages that are rendered to PDF - Part 2

In part 1, we established a connection to salesforce and issued a query with the WSC.

Now we need to make an http callout method and authenticate with the session ID from the WSC call.   For this you will need the Apache commons HTTP client (with logging and codec).

Here are the files that I imported:
  • commons-httpclient-3.0.1.jar
  • commons-logging-1.1.1.jar
  • commons-codec-1.6.jar

With these added to the build path configuration as an external jar, we are now ready to create a new class named SFDCGetApexPDF. Create the class and add the following code to it: package com.test; import org.apache.commons.httpclient.*; import org.apache.commons.httpclient.methods.*; import java.io.*; public class SFDCGetApexPDF { public boolean DownloadApexPageAsPDF(String PageURL, String OutputFileName, String accessToken) { boolean returnvalue = false; if (accessToken != null && PageURL != null & OutputFileName != null) { try { HttpClient client = new HttpClient(); GetMethod PageGet = new GetMethod(PageURL); PageGet.setRequestHeader("Authorization", "Bearer " + accessToken); int code = client.executeMethod(PageGet); System.out.println("HTTP Code: " + String.valueOf(code)); if (String.valueOf(code).contains("200")) { System.out.println(">>>> Response was 200!"); OutputStream FileOut = new FileOutputStream(OutputFileName); ByteArrayOutputStream o = new ByteArrayOutputStream(); o.write(PageGet.getResponseBody(),0,PageGet.getResponseBody().length); o.writeTo(FileOut); o.flush(); o.close(); returnvalue = true; } // look for 200 status code } catch (Exception e) { e.printStackTrace(); } // try catch } // check for null strings return returnvalue; } // DownloadApexPageAsPDF } // end class SFDCGetApexPDF

Now change your main class to look like this (changing the username,password, etc): package com.test; import com.sforce.soap.enterprise.Connector; import com.sforce.soap.enterprise.EnterpriseConnection; import com.sforce.soap.enterprise.QueryResult; import com.sforce.soap.enterprise.sobject.Opportunity; import com.sforce.ws.ConnectionException; import com.sforce.ws.ConnectorConfig; public class Main { static EnterpriseConnection connection; private static QueryResult RecordsToProcess; private static String SessionID; public static void main(String[] args) { boolean ReturnStatus; String username = "<<<<>>>>"; String password = "<<<<>>>>"; String securityToken = "<<<<>>>>"; String basePageURL = "https://c.na4.visual.force.com/apex/MyPage?id="; ReturnStatus = APILoginToSalesforce(username,password + securityToken); System.out.println("Logged in = " + ReturnStatus); if (ReturnStatus == true) { RecordsToProcess = RunSOQLQuery("SELECT ID, Name FROM Opportunity LIMIT 10"); if (RecordsToProcess.getSize() > 0) { SFDCGetApexPDF SFDownloader = new SFDCGetApexPDF(); for (int i=0;i<RecordsToProcess.getRecords().length;i++) { // cast to Opportunity Opportunity O = (Opportunity)RecordsToProcess.getRecords()[i]; System.out.println("Processing ID: " + O.getId()); String FileToMake = "C:\\temp\\ApexPage" + i + ".pdf"; String URLToGet = basePageURL + O.getId().toString(); SFDownloader.DownloadApexPageAsPDF(URLToGet,FileToMake, SessionID); } // loop thru RecordsToProcess } // RecordsToProcess.getSize() > 0 } // ReturnStatus == true } // void main private static QueryResult RunSOQLQuery(String SOQLQueryString) { QueryResult queryResults = new QueryResult(); try { queryResults = connection.query(SOQLQueryString); System.out.println("Found " + queryResults.getSize() + " records"); } catch (Exception e) { e.printStackTrace(); } return queryResults; } private static boolean APILoginToSalesforce(String USERNAME, String PASSWORD) { boolean blnAPILoginToSalesforce = false; ConnectorConfig config = new ConnectorConfig(); config.setUsername(USERNAME); config.setPassword(PASSWORD); //config.setTraceMessage(true); try { connection = Connector.newConnection(config); // display some current settings System.out.println("Auth EndPoint: "+config.getAuthEndpoint()); System.out.println("Service EndPoint: "+config.getServiceEndpoint()); System.out.println("Username: "+config.getUsername()); System.out.println("SessionId: "+config.getSessionId()); SessionID = config.getSessionId(); blnAPILoginToSalesforce = true; } catch (ConnectionException e1) { e1.printStackTrace(); } return blnAPILoginToSalesforce; } // APILoginToSalesforce } Assuming you have the visualforce page in the example and have set the correct options (username, password, etc), you should now get 10 random opportunity's PDF files in C:\temp. Of course this is just a proof of concept and you will want to remove the hard coding of the user name, password, token, path, etc and replace them with a GUI.

Downloading Visual Force pages that are rendered to PDF - Part 1

In this series of posts, we are going to look at how to build a tool in java that will:
  • Login into salesforce (via WSC and HTTP Call out with Session ID)
  • Query the database for a list of records
  • Build a URL from each record ID and use an HTTP call out to get the PDF
  • Save the PDF files to the local file system.
Being a rather technical post, you will need to already have eclipse installed and be familiar with Java development.

Start by opening eclipse (I recommend a new workspace for this) and creating a new java project.  My project was created in JavaSE-1.6.

Once your new project has been created, add a new java class (remember to generate the public static void main stub) to the src folder and give it a package name.  For the purposes of this example, we will use .com.test as the package and Main.Java as our class.

Next you will need to add the enterprise.jar and wsc-22.jar as an external jars in the reference libraries tab under the Java Build Path entry in your project properties.  Please note that you will have to generate the wsc-22.jar file.   This is because the WSDL will be different for each organization if they have custom objects.

Now add your imports (at the top of your class) for the newly added jar files


import com.sforce.soap.enterprise.Connector; import com.sforce.soap.enterprise.EnterpriseConnection; import com.sforce.soap.enterprise.QueryResult; import com.sforce.soap.enterprise.sobject.Opportunity; import com.sforce.ws.ConnectionException; import com.sforce.ws.ConnectorConfig;


Our example is going to be based on the opportunity object so if your are using a different object, be sure to change the .sobject.Opportunity line above to your object and/or add in additional objects.

Now you can add the following function and call it to get logged into salesforce:


private static boolean APILoginToSalesforce(String USERNAME, String PASSWORD) { boolean blnAPILoginToSalesforce = false; ConnectorConfig config = new ConnectorConfig(); config.setUsername(USERNAME); config.setPassword(PASSWORD); try { connection = Connector.newConnection(config); blnAPILoginToSalesforce = true; } catch (ConnectionException e1) { e1.printStackTrace(); } return blnAPILoginToSalesforce; } // APILoginToSalesforce

Don't for get to add your security token to the end of your password when calling this method. Now you can add the following method which will let you query against salesforce. private static QueryResult RunSOQLQuery(String SOQLQueryString) { QueryResult queryResults = new QueryResult(); try { queryResults = connection.query(SOQLQueryString); System.out.println("Found " + queryResults.getSize() + " records"); } catch (Exception e) { e.printStackTrace(); } return queryResults; }
Now we need a query result variable to hold our results. So add a line like this to your class:
private static QueryResult RecordsToProcess;

Now you can call the login function followed by the query function like so:

public static void main(String[] args) { boolean ReturnStatus; ReturnStatus = APILoginToSalesforce("USER@EMAIL.COM","PASSWORDandSecurityToken"); System.out.println("Logged in = " + ReturnStatus); if (ReturnStatus == true) { RecordsToProcess = RunSOQLQuery("SELECT ID, Name FROM Opportunity LIMIT 10"); } }

At this point you should now be logged into salesforce (via WSC) and able to query data.

In the part 2 of this series, we will look at how to download the files. For the full / working code of this example, please see the final post in this series.

Tuesday, June 26, 2012

Custom VF Controller + Javascript: a quick example

Update:   It appears that salesforce has fixed the issue(s) that were causing the need for the variable to be updated via javascript in the post below.   I leave the post in place for unknown future needs but as of 7/6/12 you do not need to put code in place as in the below example - it is handled automatically. 

For the advanced users, this post may be something you have already done but I post it here just in case it helps someone on a google search.

This basic example will take the text from the form, put it in a string variable and re-render it on the screen. What we want to do is modify this example so that the text gets updated upon blur or change of the text field (in addition to the button push).

Salesforce takes care of the variable getting updated upon button push due to our value="{!TextInput}" segment in the apex:inputText line.

Lets start out with a basic visual force page:

Value:{!TextInput}


And a basic controller: public with sharing class testControllerExt { public string TextInput {get; set;} public testControllerExt() { TextInput = 'Default Text'; } // constructor public pagereference ProcessForm () { return null; } } // main class

To allow the apex method to be called via javascript, we add an action function tag (setup to call the existing ProcessForm Method): You may not want this logic in your work so please notice that I have set immediate="TRUE" to avoid the full save validation that would otherwise normally occur.

This allows us to call the apex method in javascript and pass one parameter that will be assigned to our text field. No default value is assigned.

Now we will use some inline javascript and visualforce to get the element value and pass it in with some code like this: onchange="ProcessFormInJS(document.getElementById('{!$Component.TextInputField}').value);"
Visualforce will now pull in the component ID, then javascript will get the element from that ID, and finally javascript will get the text from the element value and pass it to the javascript function that will remotely call the apex function - with the variable updated.

The final code for the input text looks like this: Additional Notes: With the $component global variable automatically managing your id hierarchy, you won't have to worry about updating this code unless you change the ID of the field being used on the form.

Thursday, June 14, 2012

Changes to SOQL queries in salesforce?

Previously, I created a class that could be used to automatically send reminder emails about approvals to users.  This class has been in production with any issues or changes since my last deployment on 5/4/2012.

Yesterday, I went to deploy a totally unrelated change set that was comprised of just one new field and that field wasn't related to the approvals at all.   Upon trying to deploy this change, I was greeted with this error message:


Since Salesforce won't let you deploy code with such errors or conflicts, this code hasn't changed for months, and this error isn't related to any other logic (i.e. validation rules, etc) outside of the class, the change had to be on the Salesforce platform itself.  I do keep up with release notes and known issues, so it is a fairly safe bet to say this is an undocumented change.

After some testing, I determined that the issue was that the WI.Actor.Id was now null in the execute method's call to ApprovalReminderEmails.SendReminderEmail.

This data was previously being populated by this query:


SELECT Id, TargetObject.Name, TargetObjectID, TargetObject.Type, 
(SELECT Id, Actor.email, Actor.Name FROM WorkItems)  
FROM ProcessInstance  
WHERE Status = 'Pending' AND IsDeleted = False AND SystemModStamp < Today 
 

After some more testing, I made the query read like this (note the addition of ActorID in blue):

SELECT Id, TargetObject.Name, TargetObjectID, TargetObject.Type, 
(SELECT Id, ActorID, Actor.email, Actor.Name FROM WorkItems)  
FROM ProcessInstance  
WHERE Status = 'Pending' AND IsDeleted = False AND SystemModStamp < Today 
 

Please note that you will need to change WI.Actor.ID to WI.ActorID  in the call to ApprovalReminderEmails.SendReminderEmail as well.

Here is the full updated class: global class ApprovalReminderEmails implements Database.Batchable<sObject>,Database.Stateful { /********************************************************************* Add/Edit these constants to contain your email template names PLEASE NOTE THAT THESE MUST BE VISUALFORCE TEMPLATES TO WORK! Also, the VF template must NOT have recipientType set in the <messaging:emailTemplate> tag or you will get the error: INVALID_ID_FIELD, WhatId is not available for sending emails to UserIds ***********************************************************************/ static final string OpportunityEmailTemplateName = 'Approval_Opportunity_Reminder'; // this method is used to get all reocrds that will be processed by execute method global Database.QueryLocator start(Database.BatchableContext bc){ // try to use the .type to determine object type in SOQL // instead of DescribeSObjectResult apex code String query = 'SELECT Id, TargetObject.Name, TargetObjectID, TargetObject.Type, (SELECT Id, ActorID, Actor.email, Actor.Name FROM WorkItems) FROM ProcessInstance WHERE Status = \'Pending\' AND IsDeleted = False AND SystemModStamp < Today'; if (test.IsRunningTest() == true) { query = 'SELECT Id, TargetObject.Name, TargetObjectID, TargetObject.Type, (SELECT Id, ActorID, Actor.email, Actor.Name FROM WorkItems) FROM ProcessInstance WHERE Status = \'Pending\' AND IsDeleted = False'; } // query w/o date when test return Database.getQueryLocator(query); } // querylocator start // find and process all pending approval requests that aren't deleted // and are older than today global void execute(Database.BatchableContext BC, List<sObject> scope) { // loop thru results for (SObject s : scope) { ProcessInstance PI = (ProcessInstance)s; for(ProcessInstanceWorkitem WI : PI.WorkItems) { ApprovalReminderEmails.SendReminderEmail(PI.TargetObject.ID, PI.TargetObject.Type, WI.ActorId, WI.Actor.Email, WI.Actor.Name, PI.TargetObject.Name); } // loop thru WorkItems } // loop thru scope which contains the process instance results } // end SendReminders global void finish(Database.BatchableContext info) { } // finish private static void SendReminderEmail(Id TargetID, String TargetObjectType, Id ActorID, string ActorEmailAddress, string ActorName, string TargetDesc) { id EmailTemplateID; // target object type, name and ID will be null in the test methods but work in non test system.debug('TargetObjectType = ' + TargetObjectType); if (TargetObjectType == 'Opportunity') { EmailTemplateID = FindEmailTemplateIDByDeveloperName(OpportunityEmailTemplateName); } /** << Insert code here for other objects & email templates >> **/ // Make sure this transaction won't fail due to lack of daily capacity Messaging.reserveSingleEmailCapacity(1); Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage(); String[] toAddresses; // email will be null during testing ... if (test.IsRunningTest() == true) { toAddresses = new String[] {'no-reply@salesforce.com'}; } else { toAddresses = new String[] {ActorEmailAddress}; } // prevent Email Exception due to null address during testing if (EmailTemplateID != null) { // use our template email.setTemplateId(EmailTemplateID); email.saveAsActivity = false; // populate the template w/ object data email.setTargetObjectId(ActorID); email.setWhatId(TargetID); } else { // use a generic HTML email when no template is setup / found // get the current salesforce instance to build the link with string domain = URL.getSalesforceBaseUrl().toExternalForm(); string TargetLink = domain + '/' + string.valueof(TargetID); email.setToAddresses(toAddresses); // set the subject on the email email.setSubject('Reminder: Item pending approval'); // set the body of the email email.setHTMLBody('Dear ' + ActorName + ', <br/><br/>' + 'The following ' + TargetObjectType + ' is still pending your approval:<br/>' + TargetDesc + '<br/>' + '<br/> Click this link to view the full record:<br/> ' + '<a href=\'' + TargetLink + '\'>' + TargetLink + '</a>' + '<br/>Please DO NOT reply to this email.' ); } // check for template id // send our email by creating an array of emails and calling the send email method. Messaging.SingleEmailMessage[] EmailsToSend = new Messaging.SingleEmailMessage[] { email }; Messaging.sendEmail(EmailsToSend); } // end send reminder email // this method could be moved to a utility class and made public . . . . public static ID FindEmailTemplateIDByDeveloperName(string DeveloperName) { id EmailTemplateID; List<EmailTemplate> EmailTemplateResults = new List<EmailTemplate>(); if (DeveloperName != null) { EmailTemplateResults = [SELECT Id, Name, DeveloperName, IsActive FROM EmailTemplate WHERE DeveloperName = :DeveloperName]; if (EmailTemplateResults.size() > 0) { if (EmailTemplateResults[0].IsActive == true) { EmailTemplateID = EmailTemplateResults[0].id; } // check for active email template } // check for null search results } // check for blank name return EmailTemplateID; } // FindEmailTemplateIDByDeveloperName static testmethod void TestSendReminderEmails() { /** Create test data - edit as needed for custom rules. If you don't have approval processes in place for your test data, you will get lower code coverage in apex testing I have caught system.DMLException of NO_APPLICABLE_PROCESS to prevent total failure of the test unit. **/ // Create 5 test accounts so that only one (5 Accs + 5 Oppty) // executebatch will be invoked during testing. List <Account> AccountsToInsert = new List<Account>(); for(integer i = 0; i<5; i++) { Account a = new Account(Name='Test Account' + i); AccountsToInsert.add(a); } // loop to create 5 accounts insert AccountsToInsert; // no approval process is defined for account in this code, // so we use it to test the generic / HTML template . . . try { for(Account a: AccountsToInsert) { // Create an approval for the account and submit Approval.ProcessSubmitRequest AccountRequest = new Approval.ProcessSubmitRequest(); AccountRequest.setObjectId(a.ID); Approval.ProcessResult AccountRequestResult = Approval.Process(AccountRequest); System.assert(AccountRequestResult.isSuccess()); } // loop thru accounts submitting them for approval } catch (DMLException e) { system.debug('>>>> NO APPROVAL PROCESS FOR ACCOUNTS!'); } // Create opportunities for the accounts date myDate = date.today(); List <Opportunity> OpportunitiesToInsert = new List<Opportunity>(); for(Account a: AccountsToInsert) { Opportunity MyOppty = new opportunity(Name='Test Opportunity for ' + a.name,CloseDate=myDate,StageName='Lead'); MyOppty.AccountId = a.Id; OpportunitiesToInsert.add(MyOppty); } // loop thru accounts insert OpportunitiesToInsert; try { for(Opportunity o: OpportunitiesToInsert) { // create an approval for the opportunity Approval.ProcessSubmitRequest OpptyRequest = new Approval.ProcessSubmitRequest(); OpptyRequest.setObjectId(o.ID); Approval.ProcessResult OpptyRequestResult = Approval.Process(OpptyRequest); System.assert(OpptyRequestResult.isSuccess()); } // loop thru opportunities } catch (DMLException e) { system.debug('>>>> NO APPROVAL PROCESS FOR OPPORTUNTIES!'); } /* End create test data */ test.startTest(); ApprovalReminderEmails ARE = new ApprovalReminderEmails(); // the batch size can be no larger than 10 due to current apex email limits integer batchSize = 10; database.executebatch(ARE, batchSize); //increase code coverage by directly calling the method //since it won't get called in testing due to null values returned during testing only //I am using a standard salesforce sample here but need to change this if you removed the sample ApprovalReminderEmails.FindEmailTemplateIDByDeveloperName('ContactFollowUpSAMPLE'); test.stopTest(); } // end test method } // end class Salesforce claims that they test user classes prior to installing updates to avoid exactly this kind of issue but maybe they just missed this one. If you have any information on exactly why this change occurred or what the change was, please comment below.

P.S.:  I posted this because it is important to realize that not everything in salesforce is versioned (CSS, SOQL query optimizer, etc) and because this can result in your instance breaking without warning.  Since salesforce is really good about updating their software, this is an on going concern for each update.  Hopefully salesforce will read this and add versioning to more of the platform.