Development Patterns in Oracle Sales Cloud Application Composer, Part 1

Global Functions and Object Trigger Functions — What Goes Where

 

Introduction

Overheard the other day while walking by a new home construction site:

Electrician speaking to carpenter: “I wish I didn’t have to follow these house plans. It would be much easier if I could put some of the outlets and light switches somewhere else. And have you seen where they want me to put the main circuit box? It’s crazy. What was this architect thinking?”

Carpenter, replying: “Quit your complaining — these plans are OK. At least everything’s laid out in detail. I’ve been on some jobs where I had to decide where to place the wall studs. The plans were useless. Let’s just say that my choices came back to bite me on my last job. I had to redo seven or eight walls and an entire upstairs floor when the plumbers got on site to do their thing.”

Plumber, adding to the conversation: “No surprise — the plumbers always get the blame. But yeah, I’ve also been on jobs where I ended up getting the worst of it. On my last job, the carpenters were long gone when I found out I had no room between the wall studs to put in the PVC drain pipes and vents. Almost everything had to be rerouted. What it amounts to is this: in the end it’s the plans that are at fault.”

We could continue with this semi-hypothetical building trades conversation, but the lesson should be obvious from the short (edited for a G rating) excerpt: building a house without well-designed and thorough plans and blueprints will usually result in one disaster after another, and the amount of work and re-work required to get the job done will blow through any estimates and budgets. And this says nothing about completing construction on or before the homeowners’ scheduled move-in date, let alone how maintainable the (cobbled-together) house would be in the future.

Applying the lessons from this home construction story to software design and development is a very short logical leap: without well-designed and thorough plans for a software project, the potential need for re-work, delivery date slippages, and general overall quality of the end product, not to mention the ease of future maintenance, is directly related to having complete, detailed, and well-thought-out plans.

New building construction and ground-up software projects require plans. But what about remodeling or adding on to a house? Or, on the software side, what role does planning play when customizing or extending an existing application? For these smaller-scale projects, it may be tempting to forgo or expend less effort in the planning process. This path, however, would be ill-advised. Discounting the need for plans, even for software extension projects or home remodeling, can be dangerous for both software and building construction. With software, for all but the most trivial projects, relying on design/development patterns maximizes productivity, minimizes the need for rework and refactoring, eases code maintenance, and results in reusable code. This dictum applies to extensibility projects as much as it applies to new application development.

Taking the construction/software development analogy one step further, it is usually the role of a home architect to produce a set of house plans, as it is the responsibility of the software architect to develop plans for software projects. Both types of architects have a variety of tools at their disposal to make the design/planning process as efficient as possible. But both types of architects would be woefully unproductive if they had to start from scratch with each new project. What options are available to improve productivity?  On top of their accumulated experience, architects can take advantage of shortcuts, utilizing best practices and design patterns wherever and whenever they can. There are tried-and-true software development patterns that architects can (and should) leverage in system design. The ability of either type of architect to take advantage of and utilize best practices in their plans will only improve the quality of the plans and therefore maximize the quality and integrity of the completed building or software application.

This post illustrates several design patterns and best practices when working with Oracle Sales Cloud Application Composer. Specifically, the focus here, in Part 1 of this post, is on the relationship between global functions and object trigger functions, and how to model these functions optimally no matter what kind of extensibility is required. The use case grounding the discussion is a requirement to add enhanced document management capabilities to the Opportunity object in Sales Cloud by integrating with Oracle Document Cloud Service using REST as the communications vehicle.

Oracle Sales Cloud Customization and Extensibility Tools

Oracle Sales Cloud includes Application Composer, an integrated bundle of browser-based customization tools or tasks available to extend and/or customize the out-of-the-box application components. Oracle Sales Cloud and other related offerings in the Oracle Fusion Applications family (Marketing Cloud, etc.) can be extended with Application Composer in several ways: extending the data model by adding custom objects and building new relationships, adding new objects to the user interface, modifying the default business logic of the application by hooking into application events, enhancing the default reporting capabilities of the application, and by modifying and extending the application security model.

Within App Composer, there are a number of places where Groovy scripting can be used to hook into existing Sales Cloud processes, making it possible to add custom flows or to call out to external applications with web services. But with such a wide variety of options available, it then becomes necessary to make choices on how best to execute the integration. Lacking best practices or design patterns that apply to the requirements, it might take an architect/developer team multiple iterations before an optimal solution is discovered. Our goal here is to reduce the number of these iterations by presenting a set of generic design patterns that will apply to a multitude of extensibility use cases. Although we will be using integration examples from Oracle Documents Cloud Service, or DOCS, (which has been generally available as an Oracle PaaS offering for a month or two as of this post), the intent is to concentrate on the Application Composer side of the integration pattern rather than to drill down into the details of any specific PaaS application’s web service offerings. The discussion is devoted almost exclusively on design patterns and what belongs where in the App Composer framework. Although security, parsing methodologies, handling asynchronous calls, and other web service framework components are extremely important, they are not going to receive much attention here. Some of these components will get more attention in Part 2 of this post.

The spotlight’s focus is on global functions and an object trigger functions, and the roles they should play in external application integrations and extensions. The end result should be a representative framework of optimized, greatly reusable, global functions acting as the foundation of a reusable script library, with object-based trigger functions built on top of this foundation to address the specific business requirements. In order to be productive with Application Composer functions, it is necessary to understand Groovy scripting. Fortunately, there is an excellent guide available that not only covers generic Groovy scripting features, but also functions that are specific to using Groovy inside Application Composer. (The Release 8 version of the guide is located here: http://docs.oracle.com/cloud/latest/salescs_gs/CGSAC.pdf )

Integrating with External Applications

For integration purposes, it has become standard practice for today’s enterprise applications to expose web services interfaces as a way to communicate with other applications using industry standard protocols. SOAP (Simple Object Access Protocol) used to be the predominant protocol, but several factors, not the least of which is the ever-growing requirement to support mobile clients more efficiently, have made REST (Representational State Transfer) a better fit, and therefore more and more applications are opting for REST support, either as an adjunct to SOAP methods or as an all-out replacement for SOAP. In either case, the process flow pattern in App Composer is virtually identical for SOAP (synchronous) and REST: build a request payload, submit the request to a designated URL, receive the response payload and parse it, process the parsed response, and do something based on payload component values, assuming there are no errors.  Of course error handling becomes a required piece to the process flow, but for our introductory discussion it will be taking a back seat.

Here is a graphical depiction of the general process flow:

ProcessFlow

In many integration patterns, the same web service endpoint is the target for multiple calls at different processing points within the source application. Frequently, data returned in one response will be used to build requests for subsequent responses, hence the circular flow of the diagram.

What Lives in Global Functions

To borrow from object-oriented programming strategies, global functions should be designed for maximum re-use. Although there may be unusual situations that are exceptions to this design principle, the goal should be to maximize the potential for re-use whenever possible. In many cases, global functions can be designed to be reused across multiple applications, making them truly “global”. How is this design goal best accomplished? Here is a partial list of design strategies and objectives:

  • Minimize hard-coding. Whenever any values are hard-coded in a global function it may diminish the ability for it to be reused somewhere else. While the practice of hard-coding values may be impossible to eliminate entirely, minimizing hard-coding will pay dividends in the ability to reuse global functions to their maximum potential.
  • Liberal use of input parameters. Instead of hard-coding values inside functions pass them to the global function as parameters. This practice allows for far greater flexibility in when and where the global function can be used.
  • One job for one global function. Build the function so that it is not doing too much for its purpose. Think about multiple situations where the function will be used and then design appropriately for all of them. If the global function contains logic or flows that will not apply to these situations, extract the extraneous logic and build additional functions, if that function needs to be supported. There is a tightrope to walk at this stage of the design process:  model the function to do all that is required, but do not add so many sub-functions that it becomes too specialized.
  • Take strategic advantage of return values. Global functions can be used more flexibly if they are designed to return appropriate values to the processes that call them. In some cases it may be necessary to return multiple values to the calling process; if that is the case populate a Map object and return multiple values instead of returning single values of one type.
  • Manage the scope of error and exception handling. In general it is advisable to handle errors and exceptions as soon as possible after they occur instead of letting them bubble up into the calling stack. However, in the case of global functions, there may not be enough information or context to determine whether an event or a SOAP or REST response payload is an error condition or not. In those cases the error handling would then have to be relegated to the calling process.

When developing global functions evaluate every piece of logic and every line of code, and if certain parts of the code logic start to tie the function down to one or two business usage patterns, the function’s global nature diminishes.  Granted, occasionally there may be a legitimate need for a hybrid type of global function which is application-specific and less reusable in scope. These semi-global functions consolidate processes that are called from multiple places in a specific application.  They may contain application-specific logic which would restrict their use elsewhere.  There is a subtle difference between these application-specific global functions and generic, cross-application global functions. The former category encapsulates patterns that may be required by two or more calling object trigger functions. Properly-designed application-specific functions will normally be able to call a generic global function to get their job done. Acting as wrappers around generic global functions, they should only contain logic needed to support application-specific requirements. The bulk of the work should occur in the called generic global functions. (Part 2 of this post will explore the relationship between application-specific global functions and what we are referring to as truly cross-application global functions.)

Often it is difficult to determine where to start with the breakdown of application process flows into discrete global functions. What normally produces the optimal result is to decide on what sub-processes will be called most often from different places in the application, and work down from the “most popular” processes to those that will not be called as often. In the case of integrating with web services, starting with a skeleton of three global functions – a function that prepares the request payload, a function that calls the service, and a function for processing the response payload — is logically sound. Our high-level process flow diagram should have given us a big hint on how to break down the process into discrete functions.

In Release 8 of Oracle Sales Cloud, there are vast differences in how SOAP and REST endpoints are defined and supported in Application Composer.  With SOAP, there is a web services registration applet in Application Composer that formalizes and streamlines the definition and configuration of web services by pointing to the WSDL URL and setting up a security scheme that can be used across the active application (Common, Customer Center, Marketing, Sales, etc.) in Application Composer. The identifying name given to these connections can then be referenced in Groovy scripts, which then exposes the specific functions supported by the defined service endpoint in the Groovy Scripting palette.

By contrast, for REST endpoints there is no formal endpoint definition support as of Sales Cloud Release 8 or Release 9, and therefore all support needs to be developed from lower-level classes. One solution packaged as a global function, which utilizes lower-level Java classes) may look like this:

/*****************************************************************************************************
* Function Name: callRest
* Returns: String (JSON-formatted)
* Description: When supplied with parameters REST method (GET, POST, PUT, DELETE), resource URI extension,
and optional map for request payload parameters, returns JSON-formatted response payload String
* Example: callRest( String requestMethod, String urlExt, Map requestProps)
*
* returns response as a String or JSON-formatted ERROR String
*******************************************************************************************************/

println('Entering callRest')
def jsonInput
def respPayload
def restParamsMap = adf.util.getDocCloudParameters()
def authString = (restParamsMap.userName + ':' + restParamsMap.userPw).getBytes().encodeBase64().toString()
def fullUrlStr = restParamsMap.url + restParamsMap.restUrlExt + urlExt
def url = new URL(fullUrlStr)
HttpURLConnection connection
try {
connection = (HttpURLConnection) url.openConnection()
connection.setRequestMethod(requestMethod)
connection.setRequestProperty('Authorization', 'Basic ' + authString)
connection.setRequestProperty('Accept', 'application/json')
connection.setRequestProperty('Content-Type', 'application/json')
connection.setDoOutput(true);
connection.setDoInput(true);
def processed = false
switch (requestMethod) {
case 'PUT' :
case 'POST' :
jsonInput = ''
if (requestProps) {
jsonInput = map2Json(requestProps)
}
DataOutputStream os = new DataOutputStream(connection.getOutputStream());
os.writeBytes(jsonInput)
os.flush()
os.close()
processed = true
break
case 'GET' :
case 'DELETE' :
connection.connect()
processed = true
break
default :
println 'Unrecognized REST Method!'
}
if (processed) {
respPayload = ''
if (connection.responseCode == 200 || connection.responseCode == 201) {
respPayload = connection.content.text
} else {
respPayload = '{ERROR:' + connection.responseCode + ', URL = ' + fullUrlStr + '}'
}
} else {
respPayload = '{ERROR:Unrecognized REST Method ' + requestMethod + '}'
}
} catch (e) {
println e.getMessage()
connection?.disconnect()
}
println 'Exiting callRest'
return respPayload

After creating a properly-formatted Basic authentication string from user credentials, the function uses the HttpUrlConnection method to create a connection to the REST endpoint. Depending on the REST method that is supplied to it, the function optionally streams a request payload through the connection and saves the response, if a response is available. Otherwise it will either catch an exception or return an error string with the HTTP response code.

Note the call to the adf.util.getDocCloudParameters() in the initial part of this function.  This is a call to another global function.  In this case, the getDocCloudParameters() function satisfies a requirement that is almost universal to App Composer extensions:  because App Composer does not support global variable constructs, it has become common practice to write one global function that can provide the equivalent of global variables and constants that are used throughout the application.  (In this case the userName and userPw Map values are pulled from the function’s returned Map and are used to build the Basic authentication string.)  Building such a function allows for commonly-used hard-coded values and session-scoped variables to be maintained in a single place, and making these values available with a function call facilitates flexibility by avoiding hard-coding of values in other function code.

It is critical here to warn/note that the HttpUrlConnection method is not officially supported in Application Composer Groovy scripts as of Release 8, and that depending on Sales Cloud environment and patch bundle level the function may very well be blacklisted and unavailable for use in Groovy scripts. In the short term, or until Application Composer scripting support for REST endpoints becomes more formalized, the future for REST calls in Groovy/AppComposer is somewhat hazy. One possibility is that the URL and HttpUrlConnection methods will be given exception status by adding them to a whitelist. Another possibility is that a wrapped version of these methods (e.g. OrclHttpUrlConnection), which would prevent these methods from being used maliciously, will be made available to Groovy scripts. There could be other outcomes as well. No matter what the future holds in store, the general design pattern of this generic global function should be valid until future releases of Sales Cloud add more formal support mechanisms for outbound REST integrations.

Does this callRest function satisfy the development guidelines for a truly global function? In this case there may be some room for improvement:

  • Minimize hard-coding. This version of the function hard-codes JSON support. It would be fairly easy to extend the function to support both JSON and XML requests and responses.
  • Liberal use of input parameters. JSON/XML support options could be enabled by adding input parameter(s) that could tell the function which request/response payloads are needed/returned by a specific REST call.
  • One job for one global function. This function fulfills the one function/one job guideline, while not being too granular.
  • Take strategic advantage of return values. The function returns a String. What the calling process does with the return value can take many different routes.
  • Manage the scope of error and exception handling. Because the function is working within a network protocol layer and not a business-related layer of the application, it should manage errors only within that layer. Other exceptions, for example an error condition that may be packaged inside a returned response payload, would need to be handled by the calling process.

One other significant rationale for structuring the callRest function in this manner has to do with making any need for future maintenance as easy as possible. Knowing that it will be necessary to rewrite the function for compatibility with future releases of Sales Cloud and changing support (or lack thereof) for URL/HttpUrlConnection methods, it should be a tipoff to keep potential modifications localized to one function if at all possible. Structuring the function in this way does exactly that.

Other global functions that would normally be called by a trigger function immediately before and after calling the callRest global function are responsible for preparing the request payload (called before) and processing the response payload (called after).

The map2Json function in the Document Cloud Service integration receives a Java Map object (which Groovy can manipulate very efficiently) as an input parameter and converts it to a JSON-formatted String. There are a number of ways to get this job done; what is presented here makes use of the third-party Groovy jackson libraries that are popular with developers for supporting JSON requirements. The function could look like this:

/*************************************************************
* Function Name: map2Json
* Returns: String (JSON formatted)
* Description: Returns a JSON-formatted String that is built from a passed-in Map of key:value pairs
* Example: map2Json( Map reqParms ) returns String
**************************************************************/

println 'Entering map2Json function'
String jsonText = ''
ByteArrayOutputStream byteOut = new ByteArrayOutputStream()
org.codehaus.jackson.map.ObjectMapper mapper = new org.codehaus.jackson.map.ObjectMapper()

try {
mapper.writeValue(byteOut, (HashMap)reqParms)
} catch (org.codehaus.jackson.JsonGenerationException e1) {
e1.printStackTrace()
} catch (org.codehaus.jackson.map.JsonMappingException e2) {
e2.printStackTrace()
} catch (IOException e3) {
e3.printStackTrace()
}
jsonText = byteOut.toString()
println 'Exiting map2Json function'

return jsonText

After instantiating a new jackson ObjectMapper, the function feeds the ObjectMapper the reqParms Map object, which the function has received as an input parameter. Converting the ByteArrayOutputStream to a String is the final step before the function can return the JSON-formatted text back to the calling process.

Again, a warning is necessary here. There is no guarantee that the jackson libraries (or any other libraries such as JsonSlurper or XmlSlurper) will be available in future releases of Sales Cloud. If access from Groovy in AppComposer does get changed, it would be possible to replace dependency on the third-party libraries with a Groovy script that calls only native Java methods. Even though it means extra coding work and more lines of code, the work would be isolated to the global function and, with proper design, would not extend out to the trigger functions.

The converse function, json2Map, may look like this:

/*************************************************************
* Function Name: json2Map
* Returns: Map
* Description: Returns a Map of key:value pairs that are parsed from a passed-in JSON-formatted String
* Example: json2Map( String jsonText ) returns Map
**************************************************************/

println 'Entering json2Map function'
def map
org.codehaus.jackson.map.ObjectMapper mapper = new org.codehaus.jackson.map.ObjectMapper()
try {
map = mapper.readValue(jsonText, Map.class)
} catch (org.codehaus.jackson.JsonGenerationException e1) {
e1.printStackTrace()
} catch (org.codehaus.jackson.map.JsonMappingException e2) {
e2.printStackTrace()
} catch (IOException e3) {
e3.printStackTrace()
}
println 'Exiting json2Map function'
return map

Like the map2Json function, this function also takes advantage of the third-party jackson libraries. After instantiating a new ObjectMapper, it feeds the JSON-formatted String into the mapper and reads the value returned from the ObjectMapper as a Map, which is then returned to the calling process.

How does this pair of global functions measure up to the best practice design criteria? Again, there may be some room for marginal improvement, but not too much:

  • Minimize hard-coding. Not much apparent room for improvement.
  • Liberal use of input parameters. No need for any more parameters in its current implementation.
  • One job for one global function. These functions fulfill the one function/one job guideline, while not being too granular.
  • Take strategic advantage of return values. Return values fit the need of the calling processes, as will be seen below.
  • Manage the scope of error and exception handling. The functions include very basic exception handling at the level of the JSON processing, i.e. to handle any exceptions thrown by the jackson libraries.

Again, given the possibility that the availability of the Groovy jackson libraries that provide JSON processing support may change in future releases, these functions are built with a level of granularity that will make it far easier to rewrite them if it becomes necessary.

Object Trigger Functions – How They Should Be Designed

With the three global functions now in place to support REST calls to an external service from Sales Cloud business processes, the focus can now change to designing and building object trigger functions in Application Composer to support the business requirements. For this use case, one of the requirements is to create a dedicated opportunity folder in Document Cloud Service whenever a new opportunity is created. There are related requirements to change the Document Cloud Service folder name if the opportunity name changes, and also to delete the dedicated opportunity folder if and when the opportunity in Sales Cloud is deleted.

All three business requirements can be satisfied with object trigger functions for the Opportunity object. To get to the design-level entry point for trigger functions, open Application Composer, change to the Sales application if necessary (which is where the Opportunity object lives), open the Opportunity object, and drill down to the Server Scripts link.

From the Server Scripts work area page, click on the Triggers tab, and then either click the New icon or select Add from the Action drop-down. This will bring up the “Create Object Trigger” page. The first task on this page, which is irreversible by the way, is to decide on which trigger event to fire the function. The strategy here is to select the event which offers the highest degree of transaction integrity.

The “After Insert in Database” event is not perfect, but it does satisfy the requirement of calling the REST service to create the folder in Document Cloud Service only after the opportunity is successfully created on the Sales Cloud side. (Refer to Section 3.9 of the Release 8 Groovy Scripting Guide for a detailed list of the fifteen trigger events that can be accessed at the object level.)

Here is the implementation of the CreateNewOpportunityFolderTrigger object trigger script:

/***********************************************************
* Trigger: After Insert in Database
* Trigger Name: CreateNewOpportunityFolderTrigger
* Description: Creates new opportunity DocCloudService folder after opportunity record is written to database
************************************************************/

println 'Entering CreateNewOpportunityFolderTrigger'
def docFolderGuid = nvl(DocFolderGuid_c, '')
if (!docFolderGuid) {
def restParamsMap = adf.util.getDocCloudParameters()
def urlExt = '/folders/' + restParamsMap.topFolderGuid
def reqPayload = [name:(Name)]
def respPayload = adf.util.callRest('POST', urlExt, reqPayload)
def respMap = adf.util.json2Map(respPayload)
//TODO: better error checking required here
def newFolderGuid = respMap.id
setAttribute('DocFolderGuid_c', newFolderGuid)
println 'DocFolderGuid is ' + nvl(DocFolderGuid_c, 'null')
def urlExtSub = '/folders/' + newFolderGuid
def reqPayloadDocuments = [name:'Documents']
def respPayloadDocuments = adf.util.callRest('POST', urlExtSub, reqPayloadDocuments)
def reqPayloadSpreadsheets = [name:'Spreadsheets']
def respPayloadSpreadsheets = adf.util.callRest('POST', urlExtSub, reqPayloadSpreadsheets)
def reqPayloadPresentations = [name:'Presentations']
def respPayloadPresentations = adf.util.callRest('POST', urlExtSub, reqPayloadPresentations)
def reqPayloadPublished = [name:'Published']
def respPayloadPublished = adf.util.callRest('POST', urlExtSub, reqPayloadPublished)
} else {
println 'Opportunity folder already created for ' + Name
}
println 'Exiting CreateNewOpportunityFolderTrigger'

NOTE: The script depends upon the existence of a custom field that was added to the top-level Opportunity object: DocFolderGuid with the API name of DocFolderGuid_c.

Sequential sub-tasks that are handled by the script:

  1. 1. Checks to see if folder exists by checking if DocFolderGuid_c is null. If so, the script builds a REST endpoint URI containing the parent folder GUID under which the new opportunity folder will be created.
  2. 2. Builds a request payload consisting of a Map containing the name of the folder to be created.
  3. 3. Invokes the callRest global function to create the opportunity folder.
  4. 4. Converts the returned JSON response from the callRest functionto a Map and pulls out the GUID value for the newly-created folder.
    Assigns this value to the custom DocFolderGuid field.
  5. 5. Creates four subfolders under the dedicated opportunity folder which was just created.

By relying on calls to global functions, all of these subtasks can be done with fewer than 24 lines of code (although adding complete error/exception handling to this script would probably increase this line count significantly).

The object trigger script to handle the folder renaming is even more compact. There may be an inclination to hook into one of the field-level trigger events, which would fire after the value of a specific field has changed. But this strategy leads to problems. Testing/debugging shows that this event fires too often, and far too many network REST/HTTP calls, most of them totally unnecessary, would be generated by hooking into this event. As it turns out, if it is necessary to check for a change in the value of a field using the isAttributeChanged() function, the “Before Update in Database” event is the correct event for this object trigger script:

/***********************************************************
* Trigger: Before Update in Database
* Trigger Name: ModifyOpportunityFolderTrigger
* Description:
************************************************************/

println 'Entering ModifyOpportunityFolderTrigger'
if (isAttributeChanged('Name')) {
def reqPayloadFolder = [name:nvl(Name, 'NULL Opportunity Name -- should not happen')]
def docFolderGuid = nvl(DocFolderGuid_c, '')
if (docFolderGuid) {
def respPayloadFolder = adf.util.callRest('PUT', '/folders/' + docFolderGuid, reqPayloadFolder)
println 'respPayloadFolder'
def respMapFolder = adf.util.json2Map(respPayloadFolder)
//TODO: error checking based on respMapFolder
} else {
println 'Empty DocFolderGuid, so cannot complete operation.'
}
} else {
println 'No name change, so no folder edit.'
}
println 'Exiting ModifyOpportunityFolderTrigger'

This script initially determines if there is any work for it to do by checking to see if the value of the Opportunity Name was updated. If so, it verifies that a Document Cloud Service folder GUID has been stored for the active Opportunity, and then, after building a request payload (in this case a one-element Map key:value pair) containing the new folder name, the script calls the callRest global function to change the folder name.

One more object trigger function completes the example use case for extending default Sales Cloud Opportunity processing.   Because the global functions for REST support were designed properly (more or less), it is fairly simple to write the trigger function to delete the dedicated Document Cloud Service folder whenever a Sales Cloud Opportunity is deleted. (Instead of deleting the folder and its contents, it would be just as easy to move the document folder to an archive location, which may be a more real-world example of what a Sales Cloud user would require.)

Here is the object trigger script:

/************************************************************
* Trigger: After Delete in Database
* Trigger Name: DeleteOpportunityFolderTrigger
* Description: Will delete DocCloudService folder tied to a specific opportunity when an Opportunity object is deleted in the database
*************************************************************/

println 'Entering DeleteOpportunityFolderTrigger'
def respPayload
def docFolderGuid = nvl(DocFolderGuid_c, '')
if (docFolderGuid) {
respPayload = adf.util.callRest('DELETE','/folders/' + docFolderGuid, [:])
// TODO: error checking with response payload
println respPayload
} else {
println 'No GUID for Opportunity folder, so no folder delete'
}
println 'Exiting DeleteOpportunityFolderTrigger'

This script once again checks to see if there is a value in the DocFolderGuid custom field that points to the Opportunity-specific folder that was created in the Document Cloud Service instance. If so, it then builds a REST URI and passes control to the callRest global function. The pattern is very similar to the other object trigger functions: build the required pieces that are going to be passed to a global function, then call it, and finally process the response. The global function should be built with the appropriate try/catch structures in place so that system-level exceptions are caught and dealt with at that level. However, the situation-specific object trigger function needs to handle any higher-level exception processing that could not (should not) be caught in the global function. (More detail on exception processing will be covered in Part 2 of this post.)

Which Trigger Event for What Purpose?

The mantra of experienced Application Composer Groovy scriptwriters normally is to use the “Before…” trigger types. As the names imply, these types of functions fire before an event occurs. They are the best fit when a function (through web services calls) will be populating standard or custom field attributes prior to a transaction getting saved to the database. But there also may be a valid case for taking advantage of the “After” triggers, especially when the web service being called is creating or updating a record (or database row) of its own in another database. An example here will help to clarify. Suppose that the business requirement is for an object to be created in the target system whenever an Opportunity, Account, Contact, etc. is created in Sales Cloud. It makes more sense to have the web service call fire only after it is absolutely certain that the Sales Cloud object has been created, modified, or deleted, whatever the case may be. (More details on handling trigger events will be covered in Part 2 of this post.)

More often than not, after everything has been fleshed out, it will be necessary to refactor and possibly restructure both global and trigger functions after these components have been subjected to analysis and to unit testing. Obviously the preferred approach is to recognize the need for refactoring as early along in the development lifecycle as possible so as to minimize the amount of rework and retesting required.

Summary – Part 1

Much like building a house from the ground up or adding on to an existing home, integrating Sales Cloud with external applications using web services in Application Composer requires advance planning, but the amount of effort for planning can be minimized if best practices and proven design patterns exist, and are followed, to shortcut the design process. The design patterns presented in this post prove that reusable global functions can be built in such a way that object trigger functions handling the business logic can rely on them to do the bulk of the processing. Following this pattern will result in leaner, tighter, efficient code that is easier to maintain, and in many cases, will be reusable across multiple Sales Cloud applications. Although the context here is REST web services, following the design pattern presented here will produce the same desirable outcome for other development scenarios. The net end result is something that pleases everyone: a development team with members who can be more responsive to business end users and an organization that isn’t tied down by having to recreate the wheel every time a new business requirement surfaces.

Part 2 of this post will present other related design patterns in Application Composer and Groovy scripting, again with a grounding reference to an integration pattern between Sales Cloud and Documents Cloud Service.

Add Your Comment