Using An XSLT File to Transform XML for ANT


Hello, and welcome to Another Salesforce Blog!  Here I will be posting solutions to problems that I couldn’t find an answer to in hopes of helping those who find themselves stuck when using the Salesforce platform.

User Story

We have a new field on the Account object, and we want to prevent users from being able to edit the Account object while we deploy the new field. To do this, we will be using an ANT script to make the Account object Read Only for all profiles and permission sets, and restoring access when we are done with our changes.

Background

We have already covered the basics of how to do this in my previous post, Mass Updating Profile Object Permissions Via ANT Migration Tool. Unfortunately, manually updating profiles and permission sets is a labor intensive and error prone process. Don’t worry, there’s a better way!

XSLT stands for XSL Transformations, and is an XML styling language. What this means is we can take a chunk of XML code, and transform it into another piece of XML. Per w3Schools, “XSLT transforms an XML source-tree into an XML result-tree.” Basically, much like CSS is a defined template for how HTML is displayed, XSLT defines a template for a transform.

This tutorial assumes that you have the most recent version of Apache ANT installed, instructions for which can be found here. I have my ANT folder installed under my C:\ directory for ease of access.

Solution

We are going to use XSLT to modify existing Account object permissions on profiles to make Accounts Read Only.

Building the files

We will start by retrieving the profiles using Apache Ant. We will need three files, build.properties, build.xml, and package.xml. The package.xml file will be contained within a folder called retrievepkg. Additionally, we will have a second folder called deploypkg, which we will populate with our changes. I have my files located in the folder C:\ant\ASFB - XSLT Tutorial for ease of access.

If you are not familiar with ANT, you can refer to my previous posts, Getting Started With ANT: Retrieving Metadata via ANT Migration Tool for Salesforce, and Mass Updating Profile Object Permissions via ANT Migration Tool for detailed information on how to create the specified files.

build.properties

The build.properties file will contain information such as your username, password, package name, and login URLs.

Additionally, we are naming our blackout.package, restoration.package, and package.root filepaths, and naming our transform.xml.xslt file. We will create this file, named TransformXML.xslt, in just a moment.

The following build.properties file is an example of what we would use if we were writing this script for an enterprise organization, so that we could test in Stage before we deploy in Prod. If you are using a developer environment, you can simply specify sf.username and sf.password here.

sf.testurl = https://test.salesforce.com
sf.loginurl = https://login.salesforce.com
sf.maxPoll = 600
deployTarget = C:\\ant\\ASFB - XSLT Tutorial\\deploypkg
retrieveTarget = C:\\ant\\ASFB - XSLT Tutorial\\retrievepkg
package.root = C:\\ant\\ASFB - XSLT Tutorial\\
transform.xml.xslt = C:\\ant\\ASFB - XSLT Tutorial\\TransformXML.xslt

# stage
sf.username.stage = USERNAME.stage
sf.password.stage = PASSWORD[SECURITYTOKEN]

# prod
sf.usernameProd = USERNAME
sf.passwordProd = PASSWORD[SECURITYTOKEN]

package.xml

Our package.xml file, located in the retrievepkg and deploypkg folders, will need to pull all profiles and the Account object. It will read as follows:

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    
    <types>
        <members>Account</members>
        <name>CustomObject</name>
    </types>

    <types>
        <members>*</members>
        <name>Profile</name>
    </types>
    <version>36.0</version>
</Package>

build.xml

Next, we must write the series of commands to be executed by ANT. This will include a command to retrieve information from each environment (stage and prod), transform the information, and deploy the transformed information to the specified environment. We will also create two commands to restore the original data.

<project name="AnotherSalesforceBlog" default="test" xmlns:sf="antlib:com.salesforce">
    <property file="build.properties"/>
    <property environment="env"/>

    <taskdef resource="com/salesforce/antlib.xml" uri="antlib:com.salesforce">
        <classpath>
            <pathelement location="C:\Program Files\ant\ant-salesforce.jar" />
        </classpath>
    </taskdef>
    
    <target name="getstage">
        <sf:retrieve username="${sf.usernameStage}" 
        password="${sf.passwordStage}" 
        serverurl="${sf.testurl}" 
        maxPoll="${sf.maxPoll}" 
        retrieveTarget="retrievepkg" 
        unpackaged="retrievepkg\\package.xml" />
    </target>

    <target name="getprod">
        <sf:retrieve username="${sf.usernameProd}" 
        password="${sf.passwordProd}" 
        serverurl="${sf.loginurl}" 
        maxPoll="${sf.maxPoll}" 
        retrieveTarget="retrievepkg" 
        unpackaged="retrievepkg\\package.xml" />
    </target>

<!--
    <target name="transform">
        ...
    </target>
-->

    <target name="deployReadOnlyStage">
        <sf:deploy username="${sf.usernameStage}" 
        password="${sf.passwordStage}" 
        serverurl="${sf.testurl}" 
        maxPoll="${sf.maxPoll}" 
        deployRoot="${deployTarget}" 
        rollbackOnError="true" />
    </target>

    <target name="deployRestorationStage">
        <sf:deploy username="${sf.usernameStage}" 
        password="${sf.passwordStage}" 
        serverurl="${sf.testurl}" 
        maxPoll="${sf.maxPoll}" 
        deployRoot="${retrieveTarget}" 
        rollbackOnError="true" />
    </target>

    <target name="deployReadOnlyProd">
        <sf:deploy username="${sf.usernameProd}" 
        password="${sf.passwordProd}" 
        serverurl="${sf.loginurl}" 
        maxPoll="${sf.maxPoll}" 
        deployRoot="${deployTarget}" 
        rollbackOnError="true" />
    </target>

    <target name="deployRestorationProd">
        <sf:deploy username="${sf.usernameProd}" 
        password="${sf.passwordProd}" 
        serverurl="${sf.loginurl}" 
        maxPoll="${sf.maxPoll}" 
        deployRoot="${retrieveTarget}" 
        rollbackOnError="true" />
    </target>

</project>

For now, we will leave the transform command commented out, and we will come back to it in a moment. First, we need to write our .xslt file.

TransformXML.xslt

First, we must declare that this XSL file will be a transform, and we must declare our namespaces, which include the XSL Transform namespace and the Salesforce Metadata namespace:

<xsl:transform version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:m="http://soap.sforce.com/2006/04/metadata">
...
</xsl:transform>

Next, we will define the layout of our transform XML document:

...
  <xsl:strip-space elements="*"/>
  <xsl:output omit-xml-declaration="no" method="xml" indent="yes" />
...

The xsl:strip-space element is used to remove white-space only nodes, while the xsl:output element defines the format of the output document. We are transforming one XML document into another, new XML document. We do want the XML declaration at the top of our file, and we do want the output to be indented according to its hierarchic structure.

We want our template to match the entire document, so we use an XPath expression to define the template for the entire document, xsl: template match="/". We want our template to be applied to all children of the root node, so we use the xsl:apply-templates element with another XPath expression, select="node()|@*". This matches a node of any kind (node), allows for several paths to be selected (|), and matches any attribute node (@*). Effectively, this will iterate through the whole document.

...
 <xsl:template match="/">
    <xsl:apply-templates select="node()|@*" />
  </xsl:template>
...

Next, we want to change all editable Field Permissions to “false.” This will effectively make our Accounts read only.

Let’s break this down.

In the XML for a profile, the field permissions look like this:

Profile Field Permissions for the Account.AccountNumber field

What we are looking at is a child element (fieldPermissions) of the Profile element with three child elements (editable, field, and readable). We want to access a subchild of the root element (Profile>fieldPermissions>editable) and change it to false.

We do this by writing another match and invoking the namespace “m” for our Salesforce metadata namespace:

...
  <xsl:template 
    match="m:fieldPermissions/m:editable">
    <xsl:element name="{name()}">false</xsl:element>
  </xsl:template>
...

This looks in the XML input document for fieldPermissions elements, looks for the child elements editable, and dynamically retrieves gets the name of the output element using the name() function. The value of the element is changed to false.

Next, we want to do the same thing for object permissions.

Object permissions for the Account object

Again, this is a child element of the Profile element. We want to change the allowCreate, allowDelete, and allowEdit child elements to false so that the Account object cannot be modified while we are deploying the new field.

This will look similar to the last step, but because there are multiple child objects, we need to add an XPath expression (|) to allow for multiple paths to be selected:

...
  <xsl:template match="m:objectPermissions/m:allowCreate|m:objectPermissions/m:allowEdit|m:objectPermissions/m:allowDelete|m:objectPermissions/m:modifyAllRecords">
      <xsl:element name="{name()}">false</xsl:element>
    </xsl:template>
...

Lastly, we need to include an <xsl: copy> element to create a node with the same name, namespace, and type as the current node. Per Microsoft, this element makes identity transformation possible.

...
  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*" />
    </xsl:copy>
  </xsl:template>
...

All together, our TransformXML.xslt document will look like this:

<xsl:transform version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:m="http://soap.sforce.com/2006/04/metadata">

  <xsl:strip-space elements="*"/>
  <xsl:output omit-xml-declaration="no" method="xml" indent="yes" />

  <xsl:template match="/">
    <xsl:apply-templates select="node()|@*" />
  </xsl:template>

  <xsl:template 
    match="m:fieldPermissions/m:editable">
    <xsl:element name="{name()}">false</xsl:element>
  </xsl:template>

  <xsl:template match="m:objectPermissions/m:allowCreate|m:objectPermissions/m:allowEdit|m:objectPermissions/m:allowDelete|m:objectPermissions/m:modifyAllRecords">
      <xsl:element name="{name()}">false</xsl:element>
    </xsl:template>
    
  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*" />
    </xsl:copy>
  </xsl:template>
  
</xsl:transform>

Package.xml – transform command

Back to our package.xml document. Let’s take a look at our transform:

    <target name="transform">
        ...
    </target>

What we need to do is apply our TransformXML.xslt document to every profile in our retrievepkg folder (except for our Admin profiles, don’t want to lock ourselves out!).

    <target name="transform">
        <xslt 
        in="${retrieveTarget}\\profiles\\PROFILENAME.profile" 
        out="${deployTarget}\\profiles\\PROFILENAME.profile" 
        style="${transform.xml.xslt}">
        </xslt>
        ...
    </target>

This takes our “in” file (a profile in the retrievepkg folder), applies the transform, and outputs it as a profile in the deploypkg folder. We would complete this for every profile that we want to update in our org.

For example, if I wanted to update the Analytics Cloud Integration User profile and the Analytics Cloud Security User profile, my transform command would look like this:

    <target name="transform">
        <xslt 
        in="${retrieveTarget}\\profiles\\Analytics Cloud Integration User.profile" 
        out="${deployTarget}\\profiles\\Analytics Cloud Integration User.profile" 
        style="${transform.xml.xslt}">
        </xslt>
        <xslt 
        in="${retrieveTarget}\\profiles\\Analytics Cloud Security User.profile" 
        out="${deployTarget}\\profiles\\Analytics Cloud Security User.profile" 
        style="${transform.xml.xslt}">
        </xslt>
    </target>

We can add this back into our package.xml file, yielding the following:

<project name="AnotherSalesforceBlog" default="test" xmlns:sf="antlib:com.salesforce">
    <property file="build.properties"/>
    <property environment="env"/>

    <taskdef resource="com/salesforce/antlib.xml" uri="antlib:com.salesforce">
        <classpath>
            <pathelement location="C:\Program Files\ant\ant-salesforce.jar" />
        </classpath>
    </taskdef>
    
    <target name="getstage">
        <sf:retrieve username="${sf.usernameStage}" 
        password="${sf.passwordStage}" 
        serverurl="${sf.testurl}" 
        maxPoll="${sf.maxPoll}" 
        retrieveTarget="retrievepkg" 
        unpackaged="retrievepkg\\package.xml" />
    </target>

    <target name="getprod">
        <sf:retrieve username="${sf.usernameProd}" 
        password="${sf.passwordProd}" 
        serverurl="${sf.loginurl}" 
        maxPoll="${sf.maxPoll}" 
        retrieveTarget="retrievepkg" 
        unpackaged="retrievepkg\\package.xml" />
    </target>

    <target name="transform">
        <xslt 
        in="${retrieveTarget}\\profiles\\Analytics Cloud Integration User.profile" 
        out="${deployTarget}\\profiles\\Analytics Cloud Integration User.profile" 
        style="${transform.xml.xslt}">
        </xslt>
        <xslt 
        in="${retrieveTarget}\\profiles\\Analytics Cloud Security User.profile" 
        out="${deployTarget}\\profiles\\Analytics Cloud Security User.profile" 
        style="${transform.xml.xslt}">
        </xslt>
    </target>

    <target name="deployReadOnlyStage">
        <sf:deploy username="${sf.usernameStage}" 
        password="${sf.passwordStage}" 
        serverurl="${sf.testurl}" 
        maxPoll="${sf.maxPoll}" 
        deployRoot="${deployTarget}" 
        rollbackOnError="true" />
    </target>

    <target name="deployRestorationStage">
        <sf:deploy username="${sf.usernameStage}" 
        password="${sf.passwordStage}" 
        serverurl="${sf.testurl}" 
        maxPoll="${sf.maxPoll}" 
        deployRoot="${retrieveTarget}" 
        rollbackOnError="true" />
    </target>

    <target name="deployReadOnlyProd">
        <sf:deploy username="${sf.usernameProd}" 
        password="${sf.passwordProd}" 
        serverurl="${sf.loginurl}" 
        maxPoll="${sf.maxPoll}" 
        deployRoot="${deployTarget}" 
        rollbackOnError="true" />
    </target>

    <target name="deployRestorationProd">
        <sf:deploy username="${sf.usernameProd}" 
        password="${sf.passwordProd}" 
        serverurl="${sf.loginurl}" 
        maxPoll="${sf.maxPoll}" 
        deployRoot="${retrieveTarget}" 
        rollbackOnError="true" />
    </target>

</project>

Copy the objects folder from retrievepkg to deploypkg

In order to deploy all of the pieces in the package.xml file, we must copy the objects folder from retrievepkg to deploypkg. We are also able to remove the Account object from the package.xml file in the deploypkg folder, but I prefer to deploy everything just to be safe.

Calling the commands

Finally, we are able to execute our commands.

Open up a command line and navigate to the folder with your build.xml file. Call the command ant getstage to retrieve profile metadata from your organization.

Next, call ant transform to transform your file to make the Account object Read Only for the profiles specified in your transform command.

After the file has been transformed, we are able to call the ant deployReadOnly command for the environment we specify. This will deploy the transformed profiles in the deploy pkg folder to our environment.

Lastly, when we want to restore access to the Account object for the specified profiles, we call the ant deployRestoration command for our environment. This will deploy the profiles in our retrievepkg folder, or the folders that we got directly from Salesforce, restoring the original access.

Thanks for reading, let me know if you have any comments or questions!

Evelyn Grizzle

Another Salesforce Blog

One-Time
Monthly
Yearly

Make a one-time donation

Make a monthly donation

Make a yearly donation

Choose an amount

$5.00
$15.00
$50.00
$5.00
$15.00
$50.00
$5.00
$15.00
$50.00

Or enter a custom amount

$

Help keep Another Salesforce Blog on the internet by donating today!

Your contribution is appreciated.

Your contribution is appreciated.

DonateDonate monthlyDonate yearly

Leave a Reply

%d bloggers like this: