Thursday, January 10, 2013

404 error handler

A long time ago, we reorganized our website in such a way that all the old URLs that had been indexed in search engines and immortalized in e-mails and blog posts were suddenly nonfunctional. I considered using something like mod_rewrite to redirect the old traffic to the new URLs, but this would be problematic for some URLs (like the URLs into our forum).

Now, this isn't rocket science, but I came up with a way to have my cake and eat it too. I set up an ErrorDocument handler called 404.php that was a script that can redirect people based on their incoming URLs, Referrers, etc:
<?php                                                                          
# Useful _SERVER variables:
# REDIRECT_URL - original URL of request
# REQUEST_URI  - original URI of request?

$map = array(
    "/old/url/path" => "http://www.domain.com/new/path/"
);
if ( $map[$_SERVER['REQUEST_URI']] ) # if the request is in the map
{
    header("HTTP/1.0 302 Moved");
    header("Location: ".$map[$_SERVER['REQUEST_URI']]);
    exit();
}
if ( substr($_SERVER['REQUEST_URI'], 0, 9) == "/download" )                    
{
    header("HTTP/1.0 302 Moved");
    header("Location: http://www.domain.com/new/download/path/");
    exit();
}
?>

I checked our access logs for 404 errors a few times a week and added into the map the most common hits.
Well, I thought it was clever...

Wednesday, October 3, 2012

How to avoid hard-coding Id values

So I have seen some tricks for avoiding hard-coding Id values in your Apex code, which I think is a good practice to follow, but they've always been sort of kludgey and inelegant. Today I saw one that I didn't realize you could use, and it deserves a place here:
    Id theId = [SELECT Id FROM <table> WHERE <criteria>].Id;
This is simple and elegant and exactly what I was looking for.

For loops and SOQL queries

One of my favorite loops in Apex is the for(collection) loop. It's very useful for iterating through a collection. It allows you to do something like:
    // Fix Asset records with null Product2Id fields.
    List<Asset> assets = [SELECT Id, Product_Family__c, Product2Id
                          FROM Asset
                          WHERE Product2Id = null
                         ];
    for(Asset asset : assets)
    {
        // fix asset.Product2Id field here
    }
    update assets;
As useful as this is, it turns out that there is a better way. If you use the for([SOQL]) loop instead, Apex will actually run the SOQL query and automatically querymore each time the loop recycles. This means that you don't have to generate a complete collection and use up the heap space to store the entire query result set. This version looks like:
    // Fix Asset records with null Product2Id fields.
    List<Asset> updatedAssets = new List<Asset>();
    for(Asset asset : [SELECT Id, Product_Family__c,Product2Id
                       FROM Asset
                       WHERE Product2Id = null
                      ])
    {
        // fix asset.Product2Id here
        updatedAssets.add(asset);
    }
    update updatedAssets;
I'm not sure how to bulkify the update without maintaining a collection of records, which sort of negates the benefits (in cases like this were all records get updated anyway) of using this form of loop. But in a case where not all records would be updated this form could save considerable heap space. I didn't try it but it appears that there is another way to perform this operation:
    // Fix Asset records with null Product2Id fields.
    for(Asset[] assets : [SELECT Id, Product_Family__c,Product2Id
                          FROM Asset
                          WHERE Product2Id = null
                         ])
    {
        for(Asset asset : assets)
        {
            // fix asset.Product2Id here
        }
        update assets;
    }
This reduces the heap used to store the entire DML update at the expense of the number of DML operations performed. It will update records 200-at-a-time but only 200 record will be put onto the heap at a time. The choice of which form to use is probably situationally dependent, but knowing the options obviously is the biggest part of the battle.

Monday, April 23, 2012

Custom Bulk OpportunityLineItem Edit

So we have a business logic for handling annual maintenance contract renewals with our customers. The idea is that there is a Opportunity RecordType for renewal orders, and custom fields on the OpportunityLineItems (only accessible on the custom Opportunity type) for linking to the license records for the customer. (Custom objects are used to track the maintenance renewals.) When the Opportunity is closed, an Apex trigger creates the necessary custom objects to extend the maintenance expiration periods.

It's relatively easy to add custom formula fields to your "Opportunity Products", and you can add these fields to the "mini" Page Layout so that they appear on the Opportunity. But our reps routinely use the "Edit All" button to get the OpportunityLineItems in a grid layout and edit them all. The problem with this is that on the Edit All page there is no identifying information for each line item so it's a bit problematic and trial and error to update them all.

Professor Google showed me lots of people who felt that it should be very easy to use a VisualForce page with a custom controller to do the job. But it was short on details about how an Opportunity Controller could access the associated Line Item data. Lots of experimentation proved that it was actually very simple, once you realize that your page needs to use the Opportunity controller, not the OpportunityLineItem:
<apex:page standardController="Opportunity" showHeader="true">
  <apex:sectionHeader title="Bulk Edit for {!opportunity.Name}" description="Edit products for this opportunity." />
  <apex:messages />
  <apex:form >
  <apex:pageBlock >
  <apex:pageMessages />
  <apex:pageBlockButtons >
    <apex:commandButton value="Quick Save" action="{!quicksave}" />
    <apex:commandButton value="Save" action="{!save}" />
    <apex:commandButton value="Cancel" action="{!cancel}" />
  </apex:pageBlockButtons>
  
  <apex:pageBlockTable value="{!opportunity.OpportunityLineItems}" var="o">
    <apex:column headerValue="License" value="{!o.License_Master__r.Name}" />
    <apex:column headerValue="Product Code" value="{!o.License_Product_Code__c}" />
    <apex:column headerValue="End User" value="{!o.License_Master__r.End_User_Account__r.Name}" />
    <apex:column headerValue="Seats">
      <apex:inputField value="{!o.Quantity}" />
    </apex:column>
    <apex:column headerValue="List Price" value="{!o.MPU__c}" />
    <apex:column headerValue="Sales Price">
      <apex:inputField value="{!o.UnitPrice}" />
    </apex:column>
    <apex:column headerValue="Maintenance Term">
      <apex:inputField value="{!o.Maintenance_Term__c}" />
    </apex:column>
    <apex:column headerValue="SLA">
      <apex:inputField value="{!o.Maintenance_SLA__c}" />
    </apex:column>
    <apex:column headerValue="Maintenance Fee Override">
      <apex:inputFIeld value="{!o.Maintenance_Fee_Override__c}" />
    </apex:column>
  </apex:pageBlockTable>
  </apex:pageBlock>
  </apex:form>
</apex:page>
Ironically, by using the VisualForce page this way, it becomes very easy to add additional details from the related custom objects (for example, o.License_Master__r.End_User_Account__r.Name) without adding additional custom formula fields to the OpportunityLineItems. I suspect that if you want these fields displayed on the Opportunity detail page, you'd have to resort to formula fields.

While this page works great with respect to displaying the data we want to work with using standard Opportunity controller, clicking on the "Save" button only saves the Opportunity -- edits to the line items' details are lost. This can be corrected with a simple controller extension:
<apex:page standardController="Opportunity" extensions="OpportunityBulkLineItemEdit" showHeader="true">
  <apex:sectionHeader title="Bulk Edit for {!opportunity.Name}" description="Edit products for this opportunity." />
  <apex:messages />
  <apex:form >
  <apex:pageBlock >
  <apex:pageMessages />
  <apex:pageBlockButtons >
    <apex:commandButton value="Quick Save" action="{!quicksaveAll}" />
    <apex:commandButton value="Save" action="{!saveAll}" />
    <apex:commandButton value="Cancel" action="{!cancel}" />
  </apex:pageBlockButtons>
  
  <apex:pageBlockTable value="{!opportunity.OpportunityLineItems}" var="o">
    <apex:column headerValue="License" value="{!o.License_Master__r.Name}" />
    <apex:column headerValue="Product Code" value="{!o.License_Product_Code__c}" />
    <apex:column headerValue="End User" value="{!o.License_Master__r.End_User_Account__r.Name}" />
    <apex:column headerValue="Seats">
      <apex:inputField value="{!o.Quantity}" />
    </apex:column>
    <apex:column headerValue="List Price" value="{!o.MPU__c}" />
    <apex:column headerValue="Sales Price">
      <apex:inputField value="{!o.UnitPrice}" />
    </apex:column>
    <apex:column headerValue="Maintenance Term">
      <apex:inputField value="{!o.Maintenance_Term__c}" />
    </apex:column>
    <apex:column headerValue="SLA">
      <apex:inputField value="{!o.Maintenance_SLA__c}" />
    </apex:column>
    <apex:column headerValue="Maintenance Fee Override">
      <apex:inputFIeld value="{!o.Maintenance_Fee_Override__c}" />
    </apex:column>
  </apex:pageBlockTable>
  </apex:pageBlock>
  </apex:form>
</apex:page>
Along with the new controller extension APEX class:
public with sharing class OpportunityBulkLineItemEdit {
// Controller extension to enable saving line items edited in bulk
        private final Opportunity opp;
        private final ApexPages.StandardController sc;
        
        public OpportunityBulkLineItemEdit(ApexPages.StandardController stdCtrl) {
                this.sc = stdCtrl;
                this.opp = (Opportunity) this.sc.getRecord();
        }
        
        public PageReference saveAll() {
                update this.opp.opportunitylineitems;
                return this.sc.save();
        }
    
    public PageReference quicksaveAll() {
        update this.opp.opportunitylineitems;
        this.sc.save();
        return null;
    }
}
And, of course, it's tempting to not write tests for such a small amount of code, but in the interest of doing it right, I wrote some (that work for my environment, anyway). The trick to the tests was that we need to look up and use the products from our existing pricebooks, which as of API 23.0, are not available to tests, so we need to use the "UseAllData=true" directive:
public class OpportunityBulkLineItemEdit_Test {

    static OpportunityBulkLineItemEdit ext;
    static PageReference pref;
    static Opportunity opp;
    
    private static void init() {
        Account acct = new Account(Name = 'Test account');
        insert acct;
        
        RecordType rt = [SELECT Id FROM RecordType WHERE DeveloperName = 'SO_Maint_Renewal' AND sObjectType='Opportunity'];
        Pricebook2 pb = [SELECT Id From Pricebook2 WHERE IsStandard=true];
        
        Product2 prod = new Product2(
            Description = 'Test maintenance renewal product',
            Family = 'Maintenance',
            IsActive = true,
            Name = 'Maintenance Renewal'
            );
        insert prod;    
        
        PriceBookEntry pbe = new PricebookEntry(
            Product2Id = prod.Id,
            Pricebook2Id = pb.Id,
            UnitPrice = 0.00,
            IsActive = true,
            UseStandardPrice=FALSE
 );
        insert pbe;
        
        opp = new Opportunity(
            AccountId = acct.Id,
            Name = 'auto',
            RecordTypeId = rt.Id,
            CloseDate = date.today(),
            StageName = 'Upside',
            Pricebook2Id = pb.Id,
            Account_Rep__c = UserInfo.getUserId()
        );
        
        insert opp;

        List<license_master__c> licenses = [SELECT Id, Name FROM License_Master__c LIMIT 10];
        List<opportunitylineitem> items = new List<opportunitylineitem>();
        
        for(Integer count = 1; count < 3; count++)
        {
            OpportunityLineItem oli = new OpportunityLineItem(
                OpportunityId = opp.Id,
                PricebookEntryId = pbe.Id,
                Quantity = count * 2,
                UnitPrice = count * 10,
                License_Master__c = licenses.get(count).Id
            );
            items.add(oli);
        }
        insert items;
        
        pref = Page.BulkOLIEdit;
        pref.getParameters().put('id',opp.Id);
        Test.setCurrentPage(pref);
        
        ApexPages.StandardController con = new ApexPages.StandardController(opp);
        ext = new OpportunityBulkLineItemEdit(con);
    }
    
    @isTest (SeeAllData=true)
    static void testSaveAll()
    {
        init();
        
        Test.startTest();
        ext.saveAll();
        Test.stopTest();
    }
    
    @isTest (SeeAllData=true)
    static void testQuicksaveAll()
    {
        init();
        Test.startTest();
        ext.quicksaveAll();
        Test.stopTest();
    }  
}
And there, for posterity, is how I have implemented this functionality. Enjoy!

Thursday, February 16, 2012

Sharing ssh-agents across logins...

This is just cool.

I have this sort of weird setup.  In my day-to-day work, I need to use Windows because some tools are only available for Windows, but also because my company's products are remote connectivity products and typically have Windows clients.  So I have a Windows box with 2 20" monitors and my keyboard and mouse and all that good stuff hooked up there.

But I do most of my work in Linux.  Not only am I typically working on remote machines, either hosted at our corporate headquarters thousands of miles away, or at a datacenter somewhere, I also have my laptop here running Linux.  But to exercise our connectivity products, I usually don't work on the laptop's console.  For those cases where I do need to work on the laptop's console, I use Synergy to remotely "control" the laptop's console.  this way, I can pretend I have a 3-monitor setup, one of which just happens to be Linux.

I've had this setup for years and it's mostly worked pretty good, with one exception.  I'm forever having to start new ssh-agents and ssh-add my private keys to them.  The console is typically semi-decent, depending on how it's configured it generally has an agent autostarted and often it will ask me to unlock the key and I'll be good to go.  But all of my remote sessions start bare.

So I stumbled across a posting on SuperUser dealing with exactly this issue.  The (not top but IMHO best) answer seems to be a few simple lines added to your .bashrc:

check-ssh-agent() {
[ -S "$SSH_AUTH_SOCK" ] && { ssh-add -l >& /dev/null || \
                              [ $? -ne 2 ]; }
}
check-ssh-agent || export SSH_AUTH_SOCK=~/.tmp/ssh-agent.sock
check-ssh-agent || \
        eval "$(ssh-agent -s -a ~/.tmp/ssh-agent.sock)" > /dev/null
This will start a new agent if one doesn't exist, otherwise it will "piggy-back" onto the existing one.  Very slick, IMHO, although it doesn't interact with the keyring stuff being done on the console.  (But again, since most of my work is done via remote sessions, that's fine with me.)

Tuesday, November 8, 2011

SalesForce Test Classes

I'm no expert on SalesForce, but I've been working with it for a year now, and I think I'm starting to get why things work the way they do.  One thing I figured out today was that it would sometimes be much more helpful if my test classes could fail with a descriptive message of what went wrong instead of failing on a generic assert.  The test classes I inherited from our consultants who wrote this do things like:
System.assert(result.isSuccess==true);
This is great on the surface, and we want to test this, but when the result fails, we really want to know why it failed.  To facilitate this, I came up with a new class, testException, that I can throw when I want a test to fail:
public class testException extends Exception {}
I can then make my tests fail with better information by using this exception.  For example, the System.assert example above, could be rewritten as:
if ( !result.isSuccess)
{
    throw new testException('test failed: reason is '
                            + result.reason);
}
Now, when the test fails, it will be obvious from the error why it failed, and I can include information about what led up to the failure.  (One note is that for some reason do you have to declare your own exception class.  You cannot throw new Exception().)

Admittedly, this isn't rocket science, but I thought it made a much better style for coding your tests.

UPDATE:
Did I mention that I'm still learning all this SalesForce stuff?  If it wasn't obvious, this'll help make it readily apparent. 

So I figured something out after writing this article and playing with the tests some more.  The System.assert() method actually has an additional form that allows you to do much the same thing:
System.assert(result.isSuccess==true, "operation failed: " +
                                       result.reason);
So I'm not sure if there's really any reason to define your own exception class just for unit tests.  It might be useful if you wanted to catch the exception in some case, but my test classes aren't doing that.

Tuesday, November 1, 2011

Attention tech support: we're not idiots!

I cut my teeth doing Tech Support.  My first real job out of high school was doing some low-level engineering and technical support.   I did this for a number of years, until I eventually got the opportunity to join the development team that was being put in place.  When I got the chance, I jumped at it.  About 6 or 7 years ago, that development team was downsized.  As a result, I joined a small team of four people whose goal was to maintain and keep alive the product that had come out of that office.  For the next 4 or so years, we maintained the product as best we could.  The other three guys each sort of took ownership of parts of the product, I pitched in where I could but the I also bore the brunt of the technical support requirements for that product.  Essentially, I became a one-man technical support department, and supported all of our customers with the exception of a few very large accounts who got one-on-one support from the other developers.

So I get the support mentality.  I understand the limitations that arise when multiple product interact, and the difficulties that arise from trying to balance your obligation to support your product while avoiding trying to troubleshoot someone else's -- or worse, having to teach your customers basic things like how to edit files.  I really do get it.  I also struggled with seemingly impossible problems that occur, but you have no idea how and no way to reproduce, and very few ideas on how to troubleshoot.  I have been there.

So, why is it that when I call technical support, they treat me like a moron.  The latest example?  Salesforce.com.  Now, don't get me wrong.  My experience with Salesforce.com over the past year has been largely trial-by-fire.  I took a training class that was enough to get my feet wet, but haven't taken the "real" developer class (yet).  Despite that, I think I'm doing pretty good and have worked out most of how the system works.  The point, I guess, is that I'm no expert, but I do know mostly what I'm doing.  So when I, out of the blue, started getting error reports from Salesforce last week that said "System.LimitException: the daily limit would be exceeded by the operation" and our Lead generation process ground to a halt, I was understandably concerned.

As an aside: that's a horrible error message.  While it does tell you what is happening, sort of, it would be SO simple to note the limit that would be exceeded.  Instead, we're left not having a clue where to look.

Salesforce.com calls itself a multitenant platform, meaning that there are many organizations hosted on a single server (or likely cluster).  I have no idea how exactly they've put the platform together, but that's basically it.  Because many organizations run on a single cluster (and, I suspect, to be able to fleece you for a bit more cash), they've implemented limits on various resources throughout the platform.  And they're generally not onerous limits.  For example, a single "request" can perform up to 150 DML (similar to SQL) queries.  If you need more than that, chances are you can refactor your code into a more efficient design that doesn't actually need that many.  By enforcing that you write efficient code, they keep the platform running smoothly for everyone.  And that is cool.

But as for daily limits, there are only a few that they document (most are, as above, per request).  Your organization is limited to 1,000 API requests per day per user.  We were nowhere near this limit.  There are limits on workflows, on e-mail messages, and on "site" usage.  But we aren't using those features.  So it's very unclear what daily limit we were exceeding.  Thus, I opened a support case, simply asking them to identify the limit for us.

We're a very small customer for Salesforce.com.  So it's not surprising that we were guaranteed a 2-day response time.  After three days, I got a call from Salesforce.com support.  They wanted to do a gotomeeting session for me to show them the problem.  I was happy to do this, although by that time, we were no longer getting the error from Salesforce, so I couldn't reproduce the problem to show them.

As a second aside: I'm not going to name names, but I think that the ability to speak the language without an accent that makes you unintelligible should be a requirement for this field.  I have nothing against other nationalities or languages.  But IMHO putting a super-thick accent on the phone to do your technical support is like hiring someone who can't smell to work your perfume counter.  All it does is frustrate your customers.

Unfortunately, the code that we wrote on the Salesforce.com platform generates demo licenses for our software.  Because there are symbols in the code like "LicenseController" and "createDemoLicense", the support person assumed that the limit we were exceeding was the number of Salesforce.com licenses our organization had.  I had to argue for 10 minutes to convince her that the "licenses" in this case had nothing to do with their licenses.  I think I finally convinced her by pointing out that Salesforce.com didn't have "daily" license limits.

So she says she is going to research the problem and get back to me, which is fine by me.  Today I get a call back again.  This time, she tries to tell me that the e-mails I got were not actual exceptions, they were only warnings that we were approaching the limits for our organization.  I somewhat lost my cool and yelled at her a bit, pointing out that the exception was a System.LimitException which aborts the code that is executing and cannot be handled.  This is no warning.

Which brings me up to where we're at now.  She is still investigating the issue, which is great.  And maybe we won't be able to find out what the limit was or why we were approaching it.  I'm ok with that, if that's truly the case.  I just wish that support people would stop treating me like I'm an idiot, because I'm not.  I can't image how people like my in-laws would deal with this sort of thing.  They wouldn't have any clue whether the support person was telling the truth or making it up as they went along.

That said, I'm not sure how to solve the problem.  I get the vague impression that we've brought this on ourselves, but the solution may be more than a single person such as myself can implement in his spare time. 

Search This Blog