Pattern for handling multiple approvals at once in an approval process

I had a requirement to check for 8 different factors that could require an object to be sent for approval in an approval process. Also the approver needed to be told about all the factors that applied and approve or reject them in one go. This requirement does not fit to easily into the available approval process mechanisms. (If I’m wrong and there is a cleaner way to do this please comment.)

Here is the pattern that allows this behavior:

  • Add a text area custom field to the object called something like “ApprovalRequiredReasons”.
  • Put all the approval logic – in my case 8 checks on fields of the object and fields of parent objects – in an “Initial Submission Action” of type “Field Update” that updates the “ApprovalRequiredReasons” field. The formula builds a new-line separated text string explaining the factors that apply. Here is an example of the style of formula to use:
    IF(booleanExpression1, 'Factor 1 explanation' + BR(), '')
    + IF(booleanExpression2, 'Factor 2 explanation' + BR(), '')
    + IF(booleanExpression3, 'Factor 3 explanation' + BR(), '')
    + ...
  • Then the approval steps just reference the “ApprovalRequiredReasons” field and so avoid duplicating any of the logic. In the simplest case they can just check whether the string has a length greater than zero or not. But in my case a two step approval was sometimes required and I could achieve this by matching to two specific values in the string in two approval steps.

The email template that is sent can include the “ApprovalRequiredReasons” as can the approval page layout:

SchemaSpy Org ERD with easier setup

The code that presents a Force.com org via the JDBC metadata interfaces has been updated to make it simpler to use. This code allows tools like SchemaSpy and Open ModelSphere to work with a Force.com org.

In the case of SchemaSpy, a set of diagrams like this (and accompanying HTML descriptions of all the objects and fields) are produced:

Details at force-metadata-jdbc-driver, specifically UsageForV2.

A utility that has utility

Most code bases contain utility classes that get used when written but over time get forgotten about because their scope is too narrow. But a utility I keep coming back to in my code is this:

public class SobUtil {
    // Get the id values from a list of objects
    public static List<Id> getIds(List<SObject> sobs) {
        List<Id> ids = new List<Id>();
        for (SObject sob : sobs) {
            ids.add(sob.Id);
        }
        return ids;
    }
    // Get the id values from a specific field in a list of objects
    public static List<Id> getIds(List<SObject> sobs, SObjectField field) {
        List<Id> ids = new List<Id>();
        for (SObject sob : sobs) {
            Id theId = (Id) sob.get(field);
            if (theId != null) {
                ids.add(theId);
            }
        }
        return ids;
    }
}

There is nothing complicated here. The value is that frequently Apex code is dealing with lists of objects not single objects to keep the number of SOQL calls down. So situations where a list of id values need to be extracted are common and the utility class makes such code cleaner.

For example:

// Read the child objects for a collection of parent objects
Parent__c[] parents = ...;
Child__c[] children = [select ChildField1__c, ChildField2__c, ... from Child__c where Parent__c in :SobUtil.getIds(parents)];

Or the reverse relationship (note the expression “Child__c.Parent__c” evaluates to the field metadata type SObjectField rather than a data value; this technique provides compile-time checking that using strings to identify fields does not):

// Read the parent objects for a collection of child objects
Child__c[] children = ...;
Parent__c[] parents = [select ParentField1__c, ParentField2__c, ... from Parent__c where Id in :SobUtil.getIds(children, Child__c.Parent__c)];

Or a common situation in unit tests where to assert the changes made by a trigger the objects have to be re-read:

// Re-read a set of objects
Object__c[] objects = ...;
Object__c[] actuals = [select ObjectField1__c, ObjectField2__c, ... from Object__c where Id in :SobUtil.getIds(objects)];

But remember that where graphs of objects are involved the first port of call is SOQL relationship queries. Connecting parents and children together programmatically only makes sense in certain circumstances such as in a wizard where the list of objects selected in one page are used to query the list of objects presented in a later page.

Quick summary of how to get started with the Metadata WSDL API via Java’s JAXB

The Metadata API is used to manipulate customization information in an org including Apex classes and Visualforce pages by code such as the Force IDE. I wanted to create an Ant task to automate the creation of a large’ish number of static resources from files via this API and so following on from the Quick summary of how to get started with the Enterprise WSDL API via Java’s JAXB here are few extra things I discovered relating to the Metadata API.

The first is that the Metadata WSDL obtained from Setup -> Develop -> API -> Generate Metadata WSDL had a schema element that was missing the attribute xmlns=”http://www.w3.org/2001/XMLSchema&#8221; that puts it in the correct namespace and so this had to be manually added before the JAXB code generation would work. Also, as in the previous post, a JAXB customization file that had the targetNamespace of its bindings element set to “http://soap.sforce.com/2006/04/metadata&#8221; needed to be created.

The second is that you need classes generated from the Partner WSDL or the Enterprise WSDL to be able to do the login and obtain the session id that is then set in the session header from the Metadata WSDL. I ended up with a project with both the Partner and Metadata WSDL code generated to be able to do this. A bit of care is then needed to use the classes from the correct package.

Lastly, when you cut and paste code that relates to the Partner WSDL or Enterprise WSDL, the endpoint that you use from the login response may accidentally end up being loginResponse.getResult().getServerUrl() instead of the one you need for the Metadata WSDL case of loginResponse.getResult().getMetadataServerUrl().

And that is it if a “quick summary” is what you are interested in. If you want to see some sample code read on…

This example of how to use the Metadata API is obfuscated a little by the code being in the form of an Ant task. All you need to know to understand the Ant part is that the entry point is the execute method and attributes that have set methods can be configured via XML from a build file.

This base class handles the login and connection leaving the specifics of Metadata API or Partner API operations to extending classes:

package com.claimvantage.ant;

import java.net.URL;

import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;

import com.sforce.soap.metadata.MetadataPortType;
import com.sforce.soap.metadata.MetadataService;
import com.sforce.soap.partner.Login;
import com.sforce.soap.partner.LoginResponse;
import com.sforce.soap.partner.SforceService;
import com.sforce.soap.partner.Soap;

public abstract class ForceApiTaskBase extends Task {
    
    private String un;
    private String pw;
    
    private LoginResponse loginResponse;
    
    private MetadataPortType port;
    private com.sforce.soap.metadata.SessionHeader metaDataSessionHeader;
    
    private Soap soap;
    private com.sforce.soap.partner.SessionHeader partnerSessionHeader;

    public String getUn() {
        return un;
    }

    public void setUn(String un) {
        this.un = un;
    }

    public String getPw() {
        return pw;
    }

    public void setPw(String pw) {
        this.pw = pw;
    }

    public void execute() throws BuildException {
        try {
            doExecute();
        } catch (BuildException e) {
            throw e;
        } catch (Exception e) {
            throw new BuildException(e);
        }
    }
    
    protected abstract void doExecute() throws Exception;
    
    protected MetadataPortType getMetadataPortType() throws Exception {
        createMetadataSession();
        return port;
    }
    
    protected com.sforce.soap.metadata.SessionHeader getMetadataSessionHeader() throws Exception {
        createMetadataSession();
        return metaDataSessionHeader;
    }
    
    protected Soap getPartnerPortType() throws Exception {
        createPartnerSession();
        return soap;
    }
    
    protected com.sforce.soap.partner.SessionHeader getPartnerSessionHeader() throws Exception {
        createPartnerSession();
        return partnerSessionHeader;
    }
    
    private void createMetadataSession() throws Exception {
        
        if (port == null) {
            
            // Login done here
            createPartnerSession();
            
            MetadataService service = new MetadataService(getUrl("metadata.wsdl"), new QName("http://soap.sforce.com/2006/04/metadata", "MetadataService"));
            port = service.getMetadata();
            
            BindingProvider b = (BindingProvider) port;
            b.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, loginResponse.getResult().getMetadataServerUrl());
            
            metaDataSessionHeader = new com.sforce.soap.metadata.SessionHeader();
            metaDataSessionHeader.setSessionId(loginResponse.getResult().getSessionId());
        }
    }

    private void createPartnerSession() throws Exception {
        
        if (loginResponse == null) {
            
            SforceService service = new SforceService(getUrl("partner.wsdl"), new QName("urn:partner.soap.sforce.com", "SforceService"));
            soap = service.getSoap();
            
            // Login
            log("connecting as user " + getUn());
            Login login = new Login();
            login.setUsername(getUn());
            login.setPassword(getPw());
            
            loginResponse = soap.login(login, null, null);
            
            BindingProvider b = (BindingProvider) soap;
            b.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, loginResponse.getResult().getServerUrl());
            
            partnerSessionHeader = new com.sforce.soap.partner.SessionHeader();
            partnerSessionHeader.setSessionId(loginResponse.getResult().getSessionId());
        }
    }
    
    private URL getUrl(String name) throws Exception {
        
        // WSDL must be packaged with code
        URL url = getClass().getResource(name);
        if (url != null) {
            return url;
        } else {
            throw new Exception("Could not access WSDL from " + name);
        }
    }
}

And here is an example of an Ant task that makes use of the Metadata API. (The polling is needed to deal with the asynchronous nature of the API.) The Ant task transfers a set of files to an org as static resources:

package com.claimvantage.ant;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;

import com.sforce.soap.metadata.AsyncRequestStateType;
import com.sforce.soap.metadata.AsyncResultType;
import com.sforce.soap.metadata.CheckStatus;
import com.sforce.soap.metadata.Create;
import com.sforce.soap.metadata.Delete;
import com.sforce.soap.metadata.StaticResourceCacheControlType;
import com.sforce.soap.metadata.StaticResourceType;

/**
 * Transfer files into an org as static resources.
 */
public class LoadStaticResources extends ForceApiTaskBase {
    
    // Text between this and the extension (if present) used as the description and stripped from the resource name
    private static final String DESCRIPTION_SEPARATOR = "-";
    
    // Text after this stripped
    private static final String EXTENSION_SEPARATOR = ".";
    
    private String contentType;
    private File folder;

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    // All files in this folder pushed to server
    public void setFolder(File folder) {
        this.folder = folder;
    }

    protected void doExecute() throws Exception {
        
        if (contentType == null) {
            throw new BuildException("contentType must be set");
        }
        if (folder == null) {
            throw new BuildException("folder must be set");
        }
        if (!folder.isDirectory()) {
            throw new BuildException("folder must be a directory");
        }
        
        File[] filesArray = folder.listFiles();
        if (filesArray == null || filesArray.length == 0) {
            log(folder + " is empty");
            return;
        }
        List<File> files = Arrays.asList(filesArray);

        // API limits to this many at a time
        final int max = 10;
        for (int batch = 0; true; batch++) {
            
            int from = max * batch;
            int to = Math.min(files.size(), from + max);
            
            doBatch(files.subList(from, to));

            if (to >= files.size()) {
                break;
            }
        }
        log("Done");
    }
    
    private void doBatch(List<File> files) throws Exception {
        
        Delete delete = new Delete();
        for (File file : files) {
            StaticResourceType sr = new StaticResourceType();
            sr.setFullName(resourceName(file.getName()));
            delete.getMetadata().add(sr);
        }
        waitForCompletion(files, getMetadataPortType().delete(delete, getMetadataSessionHeader(), null).getResult(), "delete");
        
        Create create = new Create();
        for (File file : files) {
            StaticResourceType sr = new StaticResourceType();
            sr.setFullName(resourceName(file.getName()));
            sr.setDescription(resourceDecription(file.getName()));
            sr.setContent(read(file));
            sr.setContentType(contentType);
            sr.setCacheControl(StaticResourceCacheControlType.PRIVATE);
            create.getMetadata().add(sr);
        }
        waitForCompletion(files, getMetadataPortType().create(create, getMetadataSessionHeader(), null).getResult(), "create");
    }
    
    private void waitForCompletion(List<File> files, List<AsyncResultType> results, String operation) throws Exception {
        
        CheckStatus check = new CheckStatus();
        for (AsyncResultType result : results) {
            check.getAsyncProcessId().add(result.getId());
        }

        int sleepSeconds = 1;
        boolean notAllDone = true;
        while(notAllDone) {
            notAllDone = false;

            log("waiting for async result - sleeping for " + sleepSeconds + " s");
            Thread.sleep(sleepSeconds * 1000L);
            if (sleepSeconds < 64) {
                sleepSeconds *= 2;
            }

            int i = 0;
            for (AsyncResultType result : getMetadataPortType().checkStatus(check, getMetadataSessionHeader(), null).getResult()) {
                int level = result.getState() == AsyncRequestStateType.ERROR ? Project.MSG_ERR : Project.MSG_INFO;
                log(operation + " " + result.getState() + " " + files.get(i).getName() + " " + (result.getMessage() != null ? result.getMessage() : ""), level);
                if (result.getState() == AsyncRequestStateType.QUEUED || result.getState() == AsyncRequestStateType.IN_PROGRESS) {
                    notAllDone = true;
                }
                i++;
            }
        }
    }

    private String resourceName(String s) {
        
        int slash = s.indexOf(DESCRIPTION_SEPARATOR);
        int dot = s.lastIndexOf(EXTENSION_SEPARATOR);
        
        if (slash == -1) {
            slash = s.length();
        }
        if (dot == -1) {
            dot = s.length();
        }
        return s.substring(0, Math.min(slash, dot));
    }
    
    private String resourceDecription(String s) {
        
        int slash = s.indexOf(DESCRIPTION_SEPARATOR);
        int dot = s.lastIndexOf(EXTENSION_SEPARATOR);
        
        if (slash == -1) {
            return "";
        }
        if (dot == -1) {
            dot = s.length();
        }
        
        return s.substring(slash + DESCRIPTION_SEPARATOR.length(), dot);
    }
    
    private byte[] read(File file) throws IOException {
        
        log("reading " + file);
        
        // This is assuming that the data is < 5M bytes as that is the Force.com limit
        byte[] buffer = new byte[5 * 1024 * 1024];
        BufferedInputStream is = new BufferedInputStream(new FileInputStream(file));
        int size = is.read(buffer);
        
        byte[] result = new byte[size];
        System.arraycopy(buffer, 0, result, 0, size);
        return result;
    }
}

An Ant task that required the Partner API would just call the getPartnerPortType/getPartnerSessionHeader methods instead of the getMetadataPortType/getMetadataSessionHeader methods.

Quick summary of how to get started with the Enterprise WSDL API via Java’s JAXB

With JAX-WS and JAXB part of Java 6, using JAXB to handle the marshalling needed to access an org through the Enterprise WSDL API makes sense because no JARs in addition to the JRE are needed. I found this excellent series of articles Salesforce.com Partner SOAP API JAX-WS Tutorial by Marshall Pierce that covered most of what I needed (including the use of compression – very nice).

There were only two extra things to deal with:

  • The technique for setting the session id didn’t work for me so I used the wsimport tool’s -XadditionalHeaders option that allows SOAP headers (including the SessionHeader that holds the session id) to be passed in the method calls.
  • Unlike the Partner WSDL where a generic interface is used to access the SObject fields, using the Enterprise WSDL results in a class with getters and setters being generated for each SObject (strongly typed access). By default the setters and getters for primitive values like Strings were using a JAXBElement generic making for tedious coding and a crazily large ObjectFactory. Setting a generateElementProperty=”false” customization at generation time resulted in the more normal direct setters and getters.

Here is the summary part; there are just three pieces of more or less boilerplate needed…

One – the code generation (here just invoking the wsimport command-line tool through Ant):

<target name="wsimport" description="JAXB code generation">
    <exec executable="/usr/bin/wsimport">
        <arg value="-XadditionalHeaders"/>
        <arg value="-Xnocompile"/>
        <arg value="-b"/>
        <arg value="jaxb-customization.xml"/>
        <arg value="-verbose"/>
        <arg value="-d"/>
        <arg value="src-generated-jaxb"/>
        <arg value="-p"/>
        <arg value="com.sforce.soap.enterprise"/>
        <arg value="src/com/claimvantage/force/client/xml/enterprise.wsdl"/>
    </exec>
</target>

Two – the jaxb-customization.xml customization file used by the code generation:

<bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
	xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
	xmlns="http://java.sun.com/xml/ns/jaxws">
	<bindings
		node="//xsd:schema[@targetNamespace='urn:enterprise.soap.sforce.com']">
		<jaxb:globalBindings
		      underscoreBinding="asCharInWord"
		      generateElementProperty="false"
		      />
		<jaxb:schemaBindings>
			<jaxb:nameXmlTransform>
				<jaxb:typeName suffix="Type" />
			</jaxb:nameXmlTransform>
		</jaxb:schemaBindings>
	</bindings>
	<enableWrapperStyle>false</enableWrapperStyle>
	<enableAsyncMapping>false</enableAsyncMapping>
</bindings>

Three – an example of how to login and use the API (imports left out);

public class Processor {
    private Soap soap;
    private SessionHeader sessionHeader;
    private QueryOptions queryOptions;
    public Processor(String un, String pw, String st) throws Exception {
        connect(un, pw, st);
    }
    public void process() throws Exception {
        // Implement your logic here. In general case use Query/QueryMore to handle large sets of data
        List<SObject> sobs = soap.query(createQuery("select Birthdate from Contact where LastName = 'Smith'"),
                sessionHeader, queryOptions, null, null).getResult().getRecords();
        for (SObject sob : sobs) {
            Contact contact = (Contact) sob;
            System.out.println(contact.getBirthdate());
        }
    }
    private Query createQuery(String soql) {
        Query query = new Query();
        query.setQueryString(soql);
        return query;
    }
    private void connect(String un, String pw, String st) throws Exception {
        // WSDL must be packaged with code
        String wsdlPath  = "enterprise.wsdl";
        URL url = this.getClass().getResource(wsdlPath);
        if (url == null) {
            throw new Exception("Could not access WSDL from path " + wsdlPath);
        }
        // This triggers a lot of class loading
        SforceService service = new SforceService(url, new QName("urn:enterprise.soap.sforce.com", "SforceService"));
        soap = service.getSoap();
        Login login = new Login();
        login.setUsername(un);
        login.setPassword(pw + st);
        LoginResponse loginResult = soap.login(login, new LoginScopeHeader());
        BindingProvider b = (BindingProvider) soap;
        b.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, loginResult.getResult().getServerUrl());
        // Use compression for speed
        Map<String, List> httpHeaders = new HashMap<String, List>();
        httpHeaders.put("Content-Encoding", Collections.singletonList("gzip"));
        httpHeaders.put("Accept-Encoding", Collections.singletonList("gzip"));
        b.getRequestContext().put(MessageContext.HTTP_REQUEST_HEADERS, httpHeaders);
        // This must be passed with each call
        sessionHeader = new SessionHeader();
        sessionHeader.setSessionId(loginResult.getResult().getSessionId());
        // Up to this many will be returned but may be less
        queryOptions = new QueryOptions();
        queryOptions.setBatchSize(2000);
    }
}