Archive for February, 2010

Endpoint for Debugging HTTP Callouts

When I use Apex to make HTTP callouts to other web services (e.g. Google Checkout, PayPal, Shopify, MapQuest, etc), it is often a game of trial and error to get things right. Oftentimes, I get errors from the recipient that my request is invalid and, for the life of me, I cannot see the problem.

To help me debug, I keep a test endpoint in my DE orgs that I make callouts to in order to get a good look at what I am sending. It’s a simple Visualforce page that I expose over Sites to act as a fake endpoint while I do development. When developing a callout, I will often use this endpoint in the beginning to organize the parameters and then periodically throughout development to debug.

The page is simple.

<apex:page controller="Endpoint_Controller" cache="false" showHeader="false" sidebar="false" action="{!init}">
<pre>
<apex:outputText escape="false" value="{!debugInfo}"></apex:outputText>
</pre>
</apex:page>

Depending upon my need, I will add/remove the action=”{!init}” parameter from the page because I have it in there to write the {!debugInfo} variable to a Task for later review (kind of like using a task as a mini-debug log). I don’t always need it turned on, so that parameter comes and goes.

My controller, with test method, is:

public without sharing class Endpoint_Controller {

public String debugInfo {get; set;}{

debugInfo = '';

if (ApexPages.currentPage() != null){
// Incoming Headers
debugInfo += '\n***ALL INCOMING HEADERS ***\n';
for (string key: ApexPages.currentPage().getHeaders().keySet()){
if (ApexPages.currentPage().getHeaders().get(key) != null){
debugInfo += key + ' = ' + ApexPages.currentPage().getHeaders().get(key) + '\n';
}
}

// Incoming Parameters
debugInfo += '\n***ALL INCOMING PARAMETERS ***\n';
for (string key: ApexPages.currentPage().getParameters().keySet()){
if (ApexPages.currentPage().getParameters().get(key) != null){
debugInfo += key + ' = ' + ApexPages.currentPage().getParameters().get(key) + '\n';
}
}

// Other Page Reference Stuff
debugInfo += '\n***OTHER PAGE REFERENCE INFO ***\n';
debugInfo += 'Anchor: ' + ApexPages.currentPage().getAnchor() + '\n';
debugInfo += 'URL: ' + ApexPages.currentPage().getUrl() + '\n';
}

}

public Endpoint_Controller(){}

public PageReference init() {

Task t = new Task();
t.Subject = 'Endpoint Invoked';
t.Description = debugInfo;
t.ActivityDate = Date.Today();
insert t;

return null;

}

static testMethod void Endpoint_Controller_test() {

// Set the page reference and pass through some parameters
PageReference thePage = Page.Endpoint;
thePage.getParameters().put('param1','1');
thePage.getParameters().put('param2','2');
Test.setCurrentPage(thePage);

// Run the init function to have it handle the web to lead submission
Endpoint_Controller ep = new Endpoint_Controller();
ep.init();
}
}

What does it do? Well, it simply gathers up all the headers, page parameters and other useful info and outputs it in a readable way. I sent a test request to one of my pages and this is what the page produced (and logged to a task in that org):

***ALL INCOMING HEADERS ***
Accept = application/xml;charset=UTF-8
Cache-Control = no-cache, max-age=0
CipherSuite = RC4-MD5 TLSv1 128-bits
Content-Type = application/xml;charset=UTF-8
Host = testorg.secure.force.com
Pragma = no-cache
SFDC_STACK_DEPTH = 1
User-Agent = SFDC-Callout/18.0
X-Forwarded-For = 10.226.8.134
X-Salesforce-Forwarded-To = na7.salesforce.com
X-Salesforce-SIP = 204.14.234.8

***ALL INCOMING PARAMETERS ***
param1 = value1
param2 = value2
param3 = value3

***OTHER PAGE REFERENCE INFO ***
Anchor: null
URL: /apex/Endpoint?param1=value1&param2=value2&param3=value3

I also construct debugInfo variables in my Production system to capture the receipt of incoming calls to Sites pages I have exposed (e.g. Google Checkout talks to my Salesforce org via a Sites page). I will usually have a custom setting that I can turn on/off so I have an easy way to turn off the creation of those activities.

Comments (6) comments feed

Using Aggregate Functions

In Spring ’10 Salesforce released new Apex functionality for aggregate functions.  Prior to this feature being available, one would have to perform a large query, loop through it and perform calculations themselves to do things like count records, sum values, get the max value, etc.  Now, you can do it in one simple, fast query!

The API Guide has all the details about how to perform aggregate functions in SOQL, but at a high-level, they (along with the GROUP BY clause) let you do things like:

  • Get a count of Accounts grouped by Industry & State
  • Get the avg Opportunity amount grouped by Calendar Month
  • Get the sum of Custom Object $ field grouped by Account
  • etc.

Get the idea?  The functions you have available are COUNT(), COUNT_DISTINCT(), AVG(), SUM(), MIN(), MAX(). Think of these like you would the Columns To Total in a summary report.

You can also use the GROUP BY clause in your query to summarize your data by other fields.  Think of it like a Summary Report grouping.

The best way to learn it is to put it into practice.  To do so, I am going to “upgrade” the code from my Campaign Member Summary using Google Charts post.  The goal is to create a chart that looks like below:

The original controller used the following code to build the count of records per Campaign Member Status:

// List of valid Campaign Member Statuses for the Campaign
List<CampaignMemberStatus> list_cms = [select Id, Label from CampaignMemberStatus where CampaignId = :camp.id];

// Loop through each Campaign Member Status, get a count of Campaign Members and add it to the items list
for (CampaignMemberStatus cms:list_cms) {
integer Count = [select count() from CampaignMember where CampaignId = :camp.id AND Status = :cms.Label];
if (Count > 0) {
items.add(new ChartDataItem(cms.Label, Count.format()));
}
}

The code above works just fine, but has some issues. The main issue is that we are performing a query inside a for() loop. Bad! If a Campaign had more than 20 statuses, the code would fail. Also, this is a performance hit because we have to perform as many queries as there are member statuses. Enter aggregate functions.

The improved controller changes the query to work like this.

// Get all the data in one query
AggregateResult[] groupedResults = [select Status, count(Id) from CampaignMember where CampaignId = :camp.id group by status];
        
// Loop through the query and add chart items
for (AggregateResult ar : groupedResults) {
     items.add(new ChartDataItem( String.valueOf(ar.get('Status')), String.valueOf(ar.get('expr0'))));
}

Now we are doing everything we need in 1 query. Even if we had 1000 different member statuses, it wouldn’t matter. We should never hit a governor limit with this code and performance has also been improved. Some important things to note in making this change and in understanding aggregate functions:

  • Your controller class should be on API version 18.0 or higher. 18.0 is the Spring ’10 release and that’s when these functions were introduced.
  • The results of a query with aggregate functions should result in a AggregateResult[] collection.
  • The get() method is used to retrieve data from an AggregateResult object (used inside the loop when looking at a single result)
  • To get the value from a field your are grouping by (i.e. doesn’t have a function for it), use .get(‘fieldName’). In the above example, I use this to get the Status value I am grouping by.
  • To get the value from a field that has an aggregate function, use .get(‘expr#’), where # is the index number for that field. A query can have multiple functions in it so you need to specify which function you are grabbing and you do so by its index number. For you non-programmers out there, remember that counting starts at 0. In the above example, I use this to get the count(id) value. Since I only have 1 aggregate function in my query, i get it using .get(‘expr0’) where 0 is the index number for my function result.
  • The get() method returns an Object type so you will need to use the appropriate valueOf() method to put it into the data type you need.

NOTE: As of writing this post, the IDE was not yet upgraded to 18.0 so I had to do this work inside the browser and I used the new-ish Deploy functionality from inside Salesforce to migrate the change from Sandbox to Production.

Comments (20) comments feed