Sunday 25 February 2024

Zip Handling in Apex, Spring '24 Developer Preview

Image generated by DALL-E 3, based on a prompt by Bob Buzzard

Introduction

The Spring '24 release of Salesforce had some great new features and improvements, but for me some of the Beta/Developer Preview features were more interesting - for example the scratch org snapshots beta that I wrote about a few weeks ago.

I've been developing on the Salesforce platform for close to 16 years now, and I've lost count of the number of different approaches that I've taken to handle zip files. Sometimes on the server, sometimes on the client in JavaScript, but always using custom solutions that didn't perform fantastically well. The new functionality that is native to Apex won't solve this problem - governor limits still apply - that fact that it isn't implemented in Apex itself should mean that a little more processing can be wrung out of the transaction.

Trying it Out

This is a departure from previous developer preview functionality that I've tried in the past, as it's available in scratch orgs only. I'm onside with this approach as it feels like it will allow the Apex development team to be more agile and give us access to changes earlier. Anything that does go awry will disappear in a maximum of 30 days, so there won't be loads of developer editions left behind needing to be patched up. I'll take a bit of additional effort tracking down issues if it means I get my hands on things earlier.

You need to enable support for zip file processing via the features property in your scratch org definition, specifying ZipSupportInApex :

  "features": ["EnableSetPasswordInApi", "ZipSupportInApex"],

Then create a new scratch org and you are off to the races.

What Worked

Creating a zip file and then extracting it works great - I did this through some execute anonymous Apex and was pleasantly surprised there were no further hoops to jump through. As this is in developer preview I didn't expend a huge amount of effort, just adding a single entry created from a String via a Blob:

public void SimpleZip()
{
    Compression.ZipWriter writer=new Compression.ZipWriter();
    writer.addEntry('zs.csv', Blob.valueOf('Name, Title\nKeir Bowden, CEO'));
    Blob zipFile=writer.getArchive();

    ContentVersion contentVer = new ContentVersion(
        VersionData =zipFile,
        Title = 'zipsample.zip',
        Description = 'Zip Sample',
        PathOnClient = '/zipsample.zip'
    );

    insert contentVer;
}

Once I'd done this, I could see the zip and download it to extract files:

And to extract the file from the zip and debug the contents in Apex :

public void SimpleExtract()
{
    ContentVersion contentVer=[select Id, VersionData, Title, Description, PathOnClient
                                from ContentVersion
                                where Title='zipsample.zip'];

    Blob zipBody = contentVer.VersionData;
    Compression.ZipReader reader = new Compression.ZipReader(zipBody);
    Compression.ZipEntry csvEntry = reader.getEntry('zs.csv');
    Blob csv = reader.extract(csvEntry);

    System.debug('CSV body = ' + csv.toString());
}

Gave me the contents of the file that I'd created from my String :


What Didn't Work

The demo that I was actually trying to put together was the ability to choose a zip file from the content library and inspect the contents via the UI. Selecting the file was all well and good, but once I tried to extract the details of the files and wrap them in an inner class, I was out of luck.

In case it was something about my processing of the entries, I tried just debugging them :
@AuraEnabled(cacheable=true)
public static List<Entry> GetZipEntries(Id contentVersionId)
{
    List<Entry> entries=new List<Entry>();
    try 
    {
        ContentVersion contentVer=[select Id, VersionData, Title, Description, PathOnClient
                                    from ContentVersion
                                    where Id=:contentVersionId];

        Blob zipBody = contentVer.VersionData;
        Compression.ZipReader reader = new Compression.ZipReader(zipBody);
        for (Compression.ZipEntry entry : reader.getEntries())
        {
            System.debug('Entry = ' + entry.getName());
        }
    }
    catch (Exception e)
    {
        System.debug(e);
    }

    return entries;
}
but this failed in the same, odd way. No exceptions, no errors, just an exit of the server side call when I invoked ZipReader.getZipEntries().



In case it was something Lightning Components related I dusted off my Visualforce skills and tried a custom controller :
public List<Entry> getEntries()
{
    if (null==this.entries)
    {
        entries=new List<Entry>();
        
        String cvId=ApexPages.currentPage().getParameters().get('cvid');
        System.debug('Id = ' + cvId);
        ContentVersion contentVer=[select Id, VersionData, Title, Description, PathOnClient
                                    from ContentVersion
                                    where Id=:cvId];

        Blob zipBody = contentVer.VersionData;
        Compression.ZipReader reader = new Compression.ZipReader(zipBody);
        for (Compression.ZipEntry zipEntry : reader.getEntries()) 
        {
            System.debug('Processing entry');
            Entry entry=new Entry();
            entry.name=zipEntry.getName();
            entry.method=zipEntry.getMethod().name();
            entry.lastModifiedTime=zipEntry.getLastModifiedTime();
            entries.add(entry);
        }
    }

    return entries;
}
This was slightly more successful, in that it very occasionally showed the list of entries after a hard reload of the page. Most of the time though, it behaved the same and just gave up when I executed ZipReader.getEntries().



Just in case it was something about my zip file, which was an old bootstrap library I had lying around, I copied the same code into a new class and invoked that using execute anonymous.

That worked fine:



So it does appear there is an issue accessing the contents of a zip file in a transaction that originated from a custom UI, or something along those lines anyway. I couldn't find anything in the docs to indicate I shouldn't be trying this, but it is in developer preview so I'd expect a few glitches. As a wise man once said, "I'll take a bit of additional effort tracking down issues if it means I get my hands on things earlier.". 

More Information

Sunday 11 February 2024

Apex Null Coalescing Operator

Image generated by DALL-E 3 based on about 10 prompts by Bob Buzzard

Introduction

The Spring 24 release of Salesforce bring a new operator to Apex - the null coalescing operator. It makes me very happy when these kinds of operators make their way to Apex, as it feels like we're a real programming language!

The null coalescing operator consists of an operand, two question marks, and another operand:

  operand1 ?? operand2

When the expressions is evaluated, if operand 1 is not null that will be the result, otherwise operand2 will be the result. Note that this isn't a mechanism for avoiding a null result, as if operand1 is null, operand2 will be the result even if it is null.

Using the Operator

The key use case for the null coalescing operator is removing explicit, and possibly lengthy, null checks. The obvious one is falling back on a default value, so instead of :

public List<Contact> getContactsForAccount(Account account)
{
    List<Contact> contacts=contactsForAccount.get(account.Id);

    if (null==contacts)
    {
        contacts=new List<Contact>();
    }

    return contacts;
}

we can write:

public List<Contact> getContactsForAccountNew(Account account)
{
    List<Contact> contacts=contactsForAccount.get(account.Id) ??
                            new List<Contact>();

    return contacts;
}

Much more compact, but that's not all - there's another aspect to this that is good and bad in equal measure. As there is now a single line evaluating the list of contacts, a single test will give 100% coverage of the line regardless of whether the first or second operand is as the result. For some developers this is good - one less scenario needed for test coverage. For others (including me) this is bad - if both scenarios aren't tested then we don't know how it will behave when the missing scenario happens in production.

Actually there's a third aspect to this too - the right hand operand isn't evaluated if the left hand is non-null, so it's efficient and devoid of unexpected side effects.

You can also chain them. so if you have five potential candidates for an opportunity value to return, you can write:

   Opportunity opp=opp1 ?? opp2 ?? opp3 ?? opp4 ?? opp5;

Safe in the knowledge that the first non-null value will be used. 

You also don't have to use objects or primitives as the operands, but watch out for the loss of clarity if you start to put too much working into a single expression.  For example, I don't think this is the clearest way to extract a title given an Id that might refer to a Contact or a Lead:

return [select Title from Contact where Email=:email][0].Title ??
       [select Title from Lead where Email=:email limit 1].Title ??
       'No contact/lead found matching email';Al

Always think about those that come after you!

Use to pull a collection from a map or add the new collection in if it's not present. Instead of

Apples with Apples

The operands must both be the same type, so you can't have the first operand being a Lead and the second being a Contact unless they can be converted to a common type  - e.g. an sObject. By the time you introduce casting though, I'd say you've lost most of the benefit due to some ugly code:

Contact contactCand=new Contact();
Lead leadCand;

String name = (String) ((sobject) leadCand ?? (sobject) contactCand).get('Name');

You can also compare operands that can be promoted to a common type by the compiler, for example if you have some inheritance at play:

public virtual class First
{
    public String name {get; set;}
}

public class Second extends First
{
}

   ----------
   
First first=new First();
Second second=new Second();
String name=(first ?? second).name;

More Information