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” 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” 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.

Advertisements

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

  1. Hi,

    I am trying to create static resource in salesforce through java code using Metadata Api. I was referring to your code above. Can you please tell me how to get the below classes: 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;

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s