4Groovy Tips and Techniques
Groovy Tips and Techniques
This section provides a compendium of tips and techniques for getting the most out of Groovy in your application.
Accessing Current Date and Time from the Application Server
Oracle’s application development framework exposes functionality to your business object scripts through the predefined adf
variable. For example, to reference the application server's current date use the following expression:
adf.currentDate
To reference the application server's current date including the current time, use the expression:
adf.currentDateTime
Accessing Current Date and Time from the Database
Oracle’s application development framework exposes functionality to your business object scripts through the predefined adf
variable. For example, to reference the database's current date, use the following expression:
adf.currentDBDate
To reference the application server's current date including the current time, use the expression:
adf.currentDBDateTime
Understanding Additional Built-in Groovy Functions
This section explains a number of additional helper functions you can use in your scripts. Some provide a simple example as well. Use the Functions tab of the code editor palette to insert any of the built-in functions into your script.
Table Built-in Date Functions
Function |
Description |
---|---|
|
Returns: the current date, with no time Return Type: |
|
The current date and time Return Type: |
|
Returns: a date, given the year, month, and day Return Type: Parameters:
Example: to return a date for February 8th, 1998, use |
|
Returns: a timestamp, given the year, month, day, hour, minute, and second Return Type: Parameters:
Example: to return a timestamp for February 8th, 1998, at 23:42:01, use |
|
Returns: the year of a given date Return Type: Parameters:
Example: if |
|
Returns: the month of a given date Return Type: Parameters:
Example: if |
|
Returns: the day for a given date Return Type: Parameters:
Example: if |
Table Built-in String Functions
Function |
Description |
---|---|
|
Returns: Return Type: Parameters:
Example: if |
|
Returns: Return Type: Parameters:
For example, if |
|
Returns: the integer position of the first character in string Return Type: Parameters:
Example: if |
|
Returns: the first Return Type: Parameters:
Example: if |
|
Returns: the length of string Return Type: Parameters:
Example: if |
|
Returns: the string Return Type: Parameters:
Example: if |
|
Returns: the last Return Type: Parameters:
Example: if |
|
Returns: Return Type: Parameters:
Example: if |
|
Returns: the substring of Return Type: Parameters:
Examples: if |
|
Returns: the substring of Return Type: Parameters:
Example: if |
|
Returns: the string Return Type: Parameters:
Example: if |
Table Other Built-in Functions
Function |
Description |
---|---|
|
Returns: a Return Type: Parameters:
Example: |
|
Returns: a multi-valued key object for use in the Return Type: Parameters:
Example: if a standard object has a two-field key, use |
|
Returns: a key object for use in the Return Type: Parameters:
Example: if a standard object has a single-field key, as all custom objects do, use |
|
Returns: the object Return Type: Parameters:
Example: to calculate the sum of |
encodeToBase64( s ) |
Returns: the base64 encoding of Return Type: Parameters:
|
decodeBase64( s ) |
Returns: the base64 decoding of Return Type: Parameters:
|
decodeBase64ToByteArray( s ) |
Returns: byte array decoding of Return Type: Parameters:
|
encodeByteArrayToBase64( b ) |
Returns: base64 encoding of Return Type: Parameters:
|
Testing Whether a Field's Value Is Changed
You can test whether a field's value has changed in the current transaction by using the built-in isAttributeChanged()
function. As shown in this example, it takes a single string argument that provides the name of the field whose changed status you want to evaluate:
if (isAttributeChanged('Status')) { // perform some logic here in light of the fact // that status has changed in this transaction }
Avoiding Validation Threshold Errors By Conditionally Assigning Values
When you write scripts for validation rules that modify the values of fields in the current object, you must be aware of how this affects the object's so-called "validation cycle". Before allowing an object to be saved to the database, the application development framework ensures that its data passes all validation rules. The act of successfully running all defined validation rules results in the object's being marked as valid and allows the object to be saved along with all other valid objects that have been modified in the current transaction. If as part of executing a validation rule your script modifies the value of a field, this marks the object "dirty" again. This results in ADF's subjecting the object again to all of the defined validation rules to ensure that your new changes do not result in an invalid object. If the act of re-validating the object runs your scripts that modify the field values again, this process could result in a cycle that would appear to be an infinite loop. ADF avoids this possibility by imposing a limit of 10 validation cycles on any given object. If after 10 attempts at running all the rules the object still has not been able to be successfully validated due to the object's being continually modified by its validation logic, ADF will throw an exception complaining that you have exceeded the validation threshold:
Validation threshold limit reached. Invalid Entities still in cache
A simple way to avoid this from happening is to test the value of the field your script is about to assign and ensure that you perform the field assignment (or setAttribute()
call to modify its value) only if the value you intend to assign is different from its current value. An example script employing this approach would look like this:
// Object-level validation rule on a PurchaseOrder object // to derive the default purchasing rep based on a custom // algorithm defined in an object function named // determinePurchasingRep() if both the Discount and NetDaysToPay // fields have changed in the current transaction. if (isAttributeChanged('Discount') && isAttributeChanged('NetDaysToPay')) { def defaultRep = determinePurchasingRep() // If new defaultRep is not the current rep, assign it if (PurchasingRep != defaultRep) { PurchasingRep = defaultRep } } return true
Understanding "Before Commit" Performance Impact
When you write a trigger to derive field values programmatically, wherever possible use the Before Insert or Before Update triggers instead of Before Commit. When the Before Commit trigger fires, the changes in the row have already been sent to the database, and performing further field assignments therein requires doing a second round trip to the database to permanently save your field updates to each row modified in this way. When possible, using the Before-save triggers sets the field values before the changes are sent the first time to the database, resulting in better performance.
getEstimatedRowCount()
function on view object query with a complex filter, then use the
Before Commit trigger for best results. The database
COUNT()
query the function performs to return the estimate is more accurate when performed over the already-posted data changes made during the current transaction.
Detecting Row State in After Changes Posted to Database Trigger
When writing an Before Commit trigger, if your code needs to detect the effective row state of the current object, use the getPrimaryRowState()
function covered in Determining the State of a Row. For example, it can use getPrimaryRowState().isNew()
to notice that the current object was created in the current transaction or getPrimaryRowState().isModified()
to conclude instead that it was an existing row that was changed.
Avoiding Posting Threshold Errors By Conditionally Assigning Values
Despite the recommendation in Understanding “Before Commit” Performance Impact, if you still must use an Before Commit trigger then you must be aware of how this affects the object's so-called "posting cycle". For example, you might use it to perform field value assignments when your custom logic must perform a query that filters on the data being updated in the current transaction. If your trigger modifies the value of a field, this marks the object "dirty" again. This results in subjecting the object again to all of the defined validation rules to ensure that your new changes do not result in an invalid object. If the object passes validation, then your trigger's most recent field value changes must be posted again to the database. In the act of re-posting the object's changes, your trigger may fire again. If your trigger again unconditionally modifies one or more field values again, this process could result in a cycle that would appear to be an infinite loop. The runtime avoids this possibility by imposing a limit of 10 posting cycles on any given object. If after 10 attempts to post the (re)validated object to the database it remains "dirty," due to the object's being continually modified by your trigger logic, then the system will throw an exception complaining that you have exceeded the posting threshold:
Post threshold limit reached. Some entities yet to be posted
A simple way to avoid this from happening is to test the value of the field your script is about to assign and ensure that you perform the field assignment (or setAttribute()
call to modify its value) only if the value you intend to assign is different from its current value. An example script employing this approach would look like this:
// After Changes Posted in Database Trigger // If total score is 100 or more, set status to WON. def totalScore = calculateTotalScoreUsingQuery() if (totalScore >= 100) { // Only set the status to WON if it's not already that value if (Status != 'WON') { Status = 'WON' } }
Functional Restrictions in Trigger Scripts
This section documents functional restrictions of which you should be aware when writing custom Groovy script in triggers.
Before Rollback Trigger
Your trigger should not set the value of any fields in this trigger. The changes are too late to be included in the current transaction.
Passing the Current Object to a Helper Function
If an object function executes in the context of the object on which it is defined. However, if your object function needs to accept another object as a parameter, ensure that you choose Object
as the parameter’s data type.
When writing code in any trigger, object function, or other object script, you can use the expression adf.source
to pass the current object to another function you invoke that accepts a business object as a parameter.
Referencing Original Values of Changed Fields
When the value of a field gets changed during the current transaction, your code can still access the so-called "original value" of the field. This is the value it had when the existing object was retrieved from the database. Sometimes it can be useful to reference this original value as part of your business logic. To do so, use the getOriginalAttributeValue()
function as shown below (substituting your field's name for the example's priority
):
// Assume we're in context of a TroubleTicket if (isAttributeChanged('priority')) { def curPri = priority def origPri = getOriginalAttributeValue('priority') println("Priority changed: ${origPri} -> ${curPri}") // do something with the curPri and origPri values here }
Raising a Warning From a Validation Rule Instead of an Error
When your validation rule returns false
, it causes a validation error that stops normal processing. If instead you want to show the user a warning that does not prevent the data from being saved successfully, then your rule can signal a warning and then return true
. For example, your validation rule would look like this:
// if the discount is over 50%, give a warning if (Discount > 0.50) { // raise a warning using the default declarative error message adf.error.warn(null) } return true
Throwing a Custom Validation Exception
When defining object level validation rules or triggers, normally the declaratively-configured error message will be sufficient for your needs. When your validation rule returns false
to signal that the validation has failed, the error message you've configured is automatically shown to the user. The same occurs for a trigger when it calls the adf.error.raise(null)
function. If you have a number of different conditions you want to enforce, rather than writing one big, long block of code that enforces several distinct conditions, instead define a separate validation rule or trigger (as appropriate) for each one so that each separate check can have its own appropriate error message.
That said, on occasion you may require writing business logic that does not make sense to separate into individual rules, and which needs to conditionally determine which among several possible error messages to show to the user. In this case, you can throw a custom validation exception with an error string that you compose on the fly using the following technique:
// Throw a custom object-level validation rule exception // The message can be any string value throw new oracle.jbo.ValidationException('Your custom message goes here')
Note that choose this approach, your error message is not translatable in the standard way, so it becomes your responsibility to provide translated versions of the custom-thrown error messages. You could use a solution like the one presented in Returning Locale-Sensitive Custom Strings for accomplishing the job.
Returning Locale-Sensitive Custom Strings
When you throw custom validation error messages, if your end users are multi-lingual, you may need to worry about providing a locale-specific error message string. To accomplish this, you can reference the current locale (inferred from each end user’s browser settings) as part of a function that encapsulates all of your error strings. Consider a getMessage
function like the one below. Once it is defined, your validation rule or trigger can throw a locale-sensitive error message by passing in the appropriate message key:
// context is trigger or object-level validation rule throw new oracle.jbo.ValidationException(getMessage('BIG_ERROR'))
The function is defined as follows.
Function Name:
getMessage
Return Type:
String
Parameters:
stringKey String
Function Definition
// Let "en" be the default lang // Get the language part of the locale // e.g. for locale "en_US" lang part is "en" def defaultLang = 'en'; def userLocale = adf.context.getLocale() as String def userLang = left(userLocale,2) def supportedLangs=['en','it'] def lookupLang = supportedLangs.contains(userLang) ? userLang : defaultLang def messages = [BIG_ERROR: [en:'A big error occurred', it:'È successo un grande errore'], SMALL_ERROR:[en:'A small error occurred', it:'È successo un piccolo errore'] ] return messages[stringKey][lookupLang]
Raising a Trigger's Optional Declaratively-Configured Error Message
In contrast with a validation rule where the declarative error message is mandatory, when you write a trigger it is optional. Since any return value from a trigger's script is ignored, the way to cause the optional error message to be shown to the user is by calling the adf.error.raise()
method, passing null
as the single argument to the function. This causes the default declarative error message to be shown to the user and stops the current transaction from being saved successfully. For example, you trigger would look like this:
// Assume this is in a Before Insert trigger if (someComplexCalculation() == -1) { // raise an exception using the default declarative error message adf.error.raise(null) }
Accessing the View Object for Programmatic Access to Business Objects
A "view object" is a component that simplifies querying and working with business object rows. The newView()
function allows you to access a view object dedicated to programmatic access for a given business object. Each time the newView(
objectAPIName
)
function is invoked for a given value of object API name, a new view object instance is created for its programmatic access. This new view object instance is in a predictable initial state. Typically, the first thing you will then do with this new view object instance is:
Call the
findByKey()
function on the view object to find a row by key, orAppend a view criteria to restrict the view object to only return some desired subset of business objects rows that meet your needs, as described in Finding Objects Using a View Criteria.
A view object will typically be configured to return its results in sorted order. If the default sort order does not meet your needs, you can use the setSortBy()
method on the view object to provide a comma-separated list of field names on which to sort the results. The new sort order will take effect the next time you call the executeQuery()
method on the view object. See Defining the Sort Order for Query Results for further details on sorting options available.
A view object instance for programmatic access to a business object is guaranteed not to be used directly by your application user interface pages. This means that any iteration you perform on the view object in your script will not inadvertently affect the current row seen in the user interface.
Table Most Commonly Used View Object Methods
Method Name |
Description |
---|---|
|
Allows you to find a row by unique id. Returns: an array of rows having the given key, typically containing either zero or one row. Parameters:
Example: See Finding an Object by Id |
|
Allows you to find a set of matching rows based on a filter criteria. Returns: an iterator you can use to process the matching rows using methods Parameters:
Example: See Finding Rows in a Child Rowset Using findRowsMatchingCriteria |
|
Appends an additional view criteria query filter. Parameters:
Returns: - Alternatively, if you already have created a view criteria using |
|
Executes the view object's query with any currently appended view criteria filters. Returns: - |
|
Returns: - |
|
Returns: - the next row in the iterator |
|
Resets the view object's iterator to the "slot" before the first row. Returns: - |
|
Returns: - the first row in the row iterator, or |
|
Creates a new row, automatically populating its system-generated Returns: - the new row |
|
Inserts a new row into the view object's set of rows. Returns: - |
|
Set the sort order for query results. Returns: - |
Defining the Sort Order for Query Results
To define the sort order for view object query results, call the setSortBy()
method on the view object instance you are working with before calling its executeQuery()
method to retrieve the results. The setSortBy()
function takes a single string argument whose value can be a comma-separated list of one or more field names in the object. The following example shows how to use this method to sort by a single field.
def vo = newView('TroubleTicket') // Use object function to simplify filtering by agent applyViewCriteriaForSupportAnalyst(vo, analystId) vo.setSortBy('Priority') vo.executeQuery() while (vo.hasNext()) { def curRow = vo.next() // Work with current row curRow here }
By default the sort order will be ascending, but you can make your intention explicit by using the asc
or desc
keyword after the field's name in the list, separated by a space. The example below shows how to sort descending by the number of callbacks.
def vo = newView('TroubleTicket') // Use object function to simplify filtering by customer applyViewCriteriaForCustomerCode(vo, custCode) vo.setSortBy('NumberOfCallbacks desc') vo.executeQuery() while (vo.hasNext()) { def curRow = vo.next() // Work with current row curRow here }
As mentioned before, the string can be a comma-separated list of two or more fields as well. This example shows how to sort by multiple fields, including explicitly specifying the sort order.
def vo = newView('TroubleTicket') // Use object function to simplify filtering by customer applyViewCriteriaForCustomerCode(vo, custCode) // Sort ascending by Priority, then descending by date created vo.setSortBy('Priority asc, CreationDate desc') vo.executeQuery() while (vo.hasNext()) { def curRow = vo.next() // Work with current row curRow here }
By default, when sorting on a text field, its value is sorted case-senstively. A value like 'Blackberry' that starts with a capital 'B' would sort before a value like 'apple' with a lower-case 'a'. To indicate that you'd like a field's value to be sorted case-insensitively, surround the field name in the list by the UPPER()
function as shown in the following example.
def vo = newView('TroubleTicket') // Use object function to simplify filtering by customer applyViewCriteriaForCustomerCode(vo, custCode) // Sort case-insensitively by contact last name, then by priority vo.setSortBy('UPPER(ContactLastName),Priority') vo.executeQuery() while (vo.hasNext()) { def curRow = vo.next() // Work with current row curRow here }
Finding an Object by Id
To find an object by id, follow these steps:
Use the
newView()
function to obtain the view object for programmatic access for the business object in questionCall
findByKey()
, passing in a key object that you construct using thekey()
function
The new object will be saved the next time you save your work as part of the current transaction. The following example shows how the steps fit together in practice.
// Access the view object for the custom TroubleTicket object def vo = newView('TroubleTicket') def foundRows = vo.findByKey(key(100000000272002),1) def found = foundRows.size() == 1 ? foundRows[0] : null; if (found != null) { // Do something here with the found row }
To simplify the code involved in this common operation, you could consider defining the following findRowByKey()
helper function:
Function Name:
findRowByKey
Return Type:
oracle.jbo.Row
Parameters:
vo oracle.jbo.ViewObject, idValue Object
Function Definition
println('findRowByKey') def found = vo.findByKey(key(idValue),1) return found.size() == 1 ? found[0] : null;
After defining this helper function, the example below shows the simplified code for finding a row by key.
// Access the view object for the custom TroubleTicket object def vo = newView('TroubleTicket') def found = findRowByKey(vo,100000000272002) if (found != null) { // Do something here with the found row }
Finding Objects Using a View Criteria
A "view criteria" is a declarative data filter for the custom or standard objects you work with in your scripts. After creating a view object using the newView()
function, but before calling executeQuery()
on it, use the appendCriteria()
method to add a filter so the query will return only the rows you want to work with. This section explains the declarative syntax of view criteria filter expressions, and provides examples of how to use them. At runtime, the application development framework translates the view criteria into an appropriate SQL WHERE
clause for efficient database execution.
Using a Simple View Criteria
To find custom or standard objects using a view criteria, perform the following steps:
Create a view object with the
newView()
functionAppend a view criteria with the
appendViewCriteria()
function, using an appropriate filter expressionExecute the query by calling
executeQuery()
Process the results
The example below queries the TroubleTicket
object to find the trouble tickets assigned to a particular staff member with id 100000000089003
and which have a status of Working
.
/* * Query all 'Working'-status trouble tickets assigned to a staff member with id 100000000089003 */ // 1. Use the newView() function to get a view object def vo = newView('TroubleTicket') // 2. Append a view criteria using a filter expression vo.appendViewCriteria("assignedTo = 100000000089003 and status = 'Working'") // 3. Execute the query vo.executeQuery() // 4. Process the results if (vo.hasNext()) { def row = vo.next() // Do something here with the current result row }
Syntax of View Criteria Filter Expressions
You use a view criteria filter expression to identify the specific rows you want to retrieve from a view object. Each expression includes the case-sensitive name of a queriable field, followed by an operator and one or more operand values (depending on the operator used). Each operand value can be either a literal value or a bind variable value. An attempt to filter on a field that is not queriable or a field name that does not exist in the current object will raise an error. The following are simple examples of filter expressions.
To test whether a value is null
you must use the is null
or the is not null
keywords:
Comment is null
Comment is not null
For equality use the =
sign, and for inequality use either the !=
or the <>
operators. Literal datetime values must adhere exclusively to the format shown here.
NextCallSchedule = '2015-07-15 16:26:30'
Priority = 3
Priority != 1
Priority <> 1
ActivityType != 'RS'
ActivityType <> 'RS'
For relational comparisons, use the familiar <
, <=
, >
, or >
operators, along with between
or not between
. Literal date values must adhere exclusively the format shown here.
CreationDate >= '2015-07-15'
Priority <= 2
Priority < 3
Priority <> 1
Priority > 1
Priority >= 1
TotalLoggedHours >= 12.75
Priority between 2 and 4
Priority not between 2 and 4
For string matching, you can use the like
operator, employing the percent sign %
as the wildcard character to obtain "starts with", "contains", or "ends with" style filtering, depending on where you place your wildcard(s):
RecordName like 'TT-%'
RecordName like '%-TT'
RecordName like '%-TT-%'
To test whether a field's value is in a list of possibilities, you can use the in
operator:
ActivityType in ('OC','IC','RS')
You can combine expressions using the conjunctions and
and or
along with matching sets of parentheses for grouping to create more complex filters like:
(Comment is null) or ( (Priority <= 2) and (RecordName like 'TT-99%'))
(Comment is not null) and ( (Priority <= 2) or (RecordName like 'TT-99%'))
When using the between
or in
clauses, you must surround them by parentheses when you join them with other clauses using and
or or
conjunctions.
You use a filter expression in one of two ways:
Append the view criteria filter expression using
appendViewCriteria()
to a view object created usingnewView()
Create the view criteria by passing a filter expression to
newViewCriteria()
, then filter a related collection withfindRowsMatchingCriteria()
Filter expressions are not validated at design time, so if your expression contains typographical errors like misspelled field names, incorrect operators, mismatched parentheses, or other errors, you will learn of the problem at runtime when you test your business logic.
Tips for Formatting Longer Criteria Across Multiple Lines
Groovy does not allow carriage returns or newlines to appear inside of a quoted string, so for example, the following lines of script would raise an error:
def vo = newView('StaffMember') // ERROR: Single-line quotes cannot contain carriage returns or new lines vo.appendViewCriteria(" (Salary between 10000 and 24000) and JobId <> 'AD_VP' and JobId <> 'PR_REP' and CommissionPct is null and Salary != 11000 and Salary != 12000 and (DepartmentId < 100 or DepartmentId > 200) ") vo.executeQuery()
Luckily, Groovy supports the triple-quote-delimited, multi-line string literal, so you can achieve a more readable long view criteria filter expression using this as shown:
def vo = newView('StaffMember') vo.appendViewCriteria(""" (Salary between 10000 and 24000) and JobId <> 'AD_VP' and JobId <> 'PR_REP' and CommissionPct is null and Salary != 11000 and Salary != 12000 and (DepartmentId < 100 or DepartmentId > 200) """) vo.executeQuery()
Using String Substitution for Literal Values into a View Criteria Expression Used Only Once
If you will only be using a view object a single time after calling newView()
, you can use Groovy's built-in string substitution feature to replace variable or expression values directly into the view criteria expression text as shown in the following example:
def vo = newView('StaffMember') def loSal = 13500 def anon = 'Anonymous' vo.appendViewCriteria("(Salary between ${loSal} and ${loSal + 1}) and LastName != '${anon}'") vo.executeQuery()
Notice that you must still include single quotes around the literal string values. The string subsitution occurs at the moment the string is passed to the appendViewCriteria()
function, so if the values of the loSal
or anon
variables change, their new values are not reflected retroactively in the substituted string filter criteria expression. In this example below, Groovy substitutes the values of the loSal
and anon
into the view criteria expression string before passing it to the appendViewCriteria()
function. Even though their values have changed later in the script, when the vo.executeQuery()
is performed a second time, the view object re-executes using the exact same filter expression as it did before, unaffected by the changed variable values.
def vo = newView('StaffMember') def loSal = 13500 def anon = 'Anonymous' vo.appendViewCriteria("(Salary between ${loSal} and ${loSal + 1}) and LastName != '${anon}'") vo.executeQuery() // ... etc ... loSal = 24000 anon = 'Julian' // The changed values of 'loSal' and 'anon' are not used by the // view criteria expression because the one-time string substitutions // were done as part of the call to appendViewCriteria() above. vo.executeQuery()
If you need to use a view object with appended view criteria filter expression multiple times within the same script, use named bind variables as described in the following section instead of string substitution. Using named bind variables, the updated values of the variables are automatically used by the re-executed query.
Using Custom Bind Variables for View Criteria Used Multiple Times
Often you may need to execute the same view object multiple times within the same script. If your operand values change from query execution to query execution, then named bind variables allow you to append a view criteria once, and use it many times with different values for the criteria expression operands. Just add one or more named bind variables to your view object, and then set the values of these bind variables as appropriate before each execution. The bind variables act as "live" placeholders in the appended filter expression, and their current values are used each time the view object's query is executed.
To add a named bind variable, use the addBindVariable()
function. Pass a view object or rowset as the first argument and a string value to define the name of the bind variable as the second argument as shown in the example below. You can name your bind variable using any combination of letters, numbers, and underscores, as long as the name starts with a letter.
def vo = newView('StaffMember') addBindVariable(vo,'VarLastName') setBindVariable(vo,'VarLastName','King') vo.appendViewCriteria('LastName = :VarLastName') vo.executeQuery() while (vo.hasNext()) { def r = vo.next(); // Will return "Steven King" and "Janette King" } setBindVariable(vo,'VarLastName','Higgins') vo.executeQuery() while (vo.hasNext()) { def r = vo.next(); // Will return "Shelley Higgins" }
You can reference a named bind variable in the view criteria expression anywhere a literal value can be used, prefacing its name by a colon (e.g. :VarLastName
). After adding the bind variable, you use the setBindVariable()
function one or more times in your script to assign values to the variable. Until you explicitly set its value for the current view object or rowset, your bind variable defaults to having a value of null
. Accidentally leaving the value null
will result in retrieving no rows for most filter expressions involving a bind variable operand due to how the SQL language treats the null
value in comparisons. The current value of the bind variable is used each time your script executes the view object. In the example below, this causes the rows for employees "Steven King" and "Janette King" to be returned during the first view object execution, and the row for "Shelly Higgins" to be returned on the second view object execution.
By default, the data type of the named bind variable is of type Text
. If you need to use a bind variable in filter expressions involving number, date, or datetime fields, then you need to explicitly define a bind variable with the appropriate type for best performance. To add a bind variable of a specific datatype, pass one of the values Text
, Number
, Date
, or Datetime
as a string value to the optional third argument of the addBindVariable()
function. For example, the following script uses two bind variables of type Number
and another of type Date
. Notice that the data type name is not case-sensitive (e.g. Number
, number
, or NUMBER
are all allowed).
def vo = newView('TroubleTicket') addBindVariable(vo,'VarLowPri','number') addBindVariable(vo,'VarHighPri','Number') addBindVariable(vo,'VarDueDate','DATE') setBindVariable(vo, 'VarLowPri', 1) setBindVariable(vo, 'VarDueDate', 2) setBindVariable(vo, 'VarDueDate', today() + 3) vo.appendViewCriteria('(priority between :VarLowPri and :VarHighPri) and dueDate < :VarDueDate') vo.executeQuery() while (vo.hasNext()) { def row = vo.next() // Returns trouble tickets with priorities 1 and 2 that are // due within three days from today } setBindVariable(vo, 'VarLowPri', 3) setBindVariable(vo, 'VarDueDate', 4) setBindVariable(vo, 'VarDueDate', today() + 5) vo.executeQuery() while (vo.hasNext()) { def row = vo.next() // Returns trouble tickets with priorities 3 and 4 that are // due within five days from today }
Using View Criteria to Query Case-Insensitively
If you want to filter in a case-insensitive way, you can use the upper()
function around the field name in the filter. If you are not sure whether the operand value is uppercase, you can also use the upper()
function around the operand like this:
upper(JustificationCode) = 'BRK'
upper(JustificationCode) = upper(:codeVar)
upper(JustificationCode) like upper(:codeVar)||'%'
Limitations of View Criteria Filter Expressions
While view criteria filter expressions are extremely convenient, they do not support every possible type of filtering that you might want to do. This section describes several constructs that are not possible to express directly, and where possible, suggests an alternative way to achieve the filtering.
Only a case-sensitive field name is allowed before the operator
On the left hand side of the operator, only a case-sensitive field name is allowed. So, for example, even a simple expression like
1 = 1
is considered illegal because the left-hand side is not a field name.Cannot reference a calculated expression directly as an operand value
You might be interested in querying all rows where one field is equal to a calculated quantity. For example, when querying trouble tickets you might want to find all open tickets whose Resolution Promised Date is less than three days away. Unfortunately, an expression like
ResolutionPromisedDate <= today() + 3
is not allowed because it uses a calculated expression on the right hand side of the operator. As an alternative, you can compute the value of the desired expression prior to appending the view criteria and use the already-computed value as a literal operand value string substitution variable in the string or as the value of a bind variable.Cannot reference a field name as an operand value
You might be interested in querying all rows where one field is equal to another field value. For example, when querying contacts you might want to find all contacts whose Home Phone Number is equal to their Work Phone Number. Unfortunately, an expression like
HomePhoneNumber = WorkPhoneNumber
is not allowed because it uses a field name on the right hand side of the operator. A clause such as this will be ignored at runtime, resulting in no effective filtering.Cannot reference fields of related objects in the filter expression
It is not possible to reference fields of related objects directly in the filter query expression. As an alternative, you can reference the value of a related expression prior to appending the view criteria and use the already-computed value as a literal operand value string substitution variable in the string or as the value of a bind variable.
Cannot use bind variable values of types other than Text, Number, Date, or Datetime
It is not possible to use bind variable values of types other than the four supported types: Text, Number, Date, and Datetime. An attempt to use other data types as the value of a bind variable may result in errors or in the criteria's being ignored.
Finding Rows in a Child Rowset Using findRowsMatchingCriteria
In addition to using view criteria to filter a view object that you create using newView()
, you can also use one to retrieve a subset of the rows in a related collection. For example, if a TroubleTicket custom object contains a child object collection of related activities, you can process selected activities in the related collection using code as shown below:
def vo = newView('TroubleTicket') vo.appendViewCriteria("priority = 1 and status = 'Open'") vo.executeQuery() def vc = null // Process all open P1 trouble tickets while (vo.hasNext()) { def curTicket = vo.next() def activities = curTicket.activityCollection if (vc == null) { addBindVariable(activities,'TodaysDate','date') vc = newViewCriteria(activities,"activityType in ('OC','IC') and creationDate > :TodaysDate") } // Process the activities created today for inbound/outbound calls setBindVariable(activities,'TodaysDate',today()) def iter = activities.findRowsMatchingCriteria(vc,-1) while (iter.hasNext()) { def activity = iter.next() // process the activity here } }
The newViewCriteria()
function accepts an optional third parameter ignoreNullBindVarValues
of boolean type that you can use to indicate whether filter expression predicates containing null bind variable values should be ignored. If omitted, the default value of this parameter is false.
Accomplishing More with Less Code
Your code will frequently work with collections and contain conditional logic and loops involving values that might be null
. This section explains the simplest way of working with conditionals and loops when the value involved might be null
, and covers how to define and pass functions around like objects using closures. Finally, it explains the most common collection methods and how to combine them with closures to gain maximum expressive power in minimum lines of code. Fewer lines of code makes your business logic easier to read and write.
Embracing Null-Handling in Conditions
null
values. If a variable
someFlag
is a
Boolean
variable that might be
null
, then the following conditional block executes only if
someFlag
is
true
. If
someFlag
is
null
or
false
, then the block is skipped.
// If boolean someFlag is true... if (someFlag) { // Do something here if someFlag is true }
A String
variable can be null
, an empty string (""
), or can contain at least one character in it. If a variable middleName
is a String
, then the following conditional block executes only if middleName
is not null
and contains at least one character:
// If customer has a middle name... if (middleName) { // Do something here if middleName has at least one character in it }
If a variable recentOrders
is a List
, then the following conditional block executes only if recentOrders
is not null
and contains at least one element:
// If customer has any recent orders... if (recentOrders) { // Do something here if recentOrders has at least one element }
recentTransactions
is a
Map
, then the following conditional block executes only if
recentTransactions
is not
null
and contains at least one map entry:
// If supplier has any recent transactions... if (recentTransactions) { // Do something here if recentTransactions has at least one map entry }
customerId
can be
null
, and its data type is anything other than the ones described above then the following conditional block executes only if
customerId
has a non-
null
value:
// If non-boolean customerId has a value... if (customerId) { // Do something here if customerId has a non-null value }
Map
entry in a conditional and there's a chance the
Map
might be
null
, then remember to use the safe-navigation operator (
?.
) when referencing the map key by name:
// Use the safe-navigation operator in case options Map is null if (options?.orderBy) { // Do something here if the 'orderBy' key exists and has a non-null value }
Embracing Null-Handling in Loops
You can avoid many extra lines of code by understanding how loops behave with null
values. If a variable recentOrders
is a List
, then the following loop processes each element in the list or gets skipped if the variable is null
or the list is empty:
// Process recent customer orders (if any, otherwise skip) for (order in recentOrders) { // Do something here with current order }
recentTransactions
is a
Map
, then the following conditional block executes only if
recentTransactions
is not
null
and contains at least one map entry:
// Process supplier's recent transaction (if any, otherwise skip) for (transaction in recentTransactions) { // Do something here with each transaction referencing each map // entry's key & value using transaction.key & transaction.value }
A String
variable can be null
, an empty string (""
), or can contain at least one character in it. If a variable middleName
is a String
, then the following conditional block will execute only if middleName
is not null
and contains at least one character:
// Process the characters in the customer's middle name for (c in middleName) { // Do something here with each character 'c' }
If your for
loop invokes a method directly on a variable that might be null
, then use the safe-navigation operator (?.
) to avoid an error if the variable is null
:
// Split the recipientList string on commas, then trim // each email to remove any possible whitespace for (email in recipientList?.split(',')) { def trimmedEmail = email.trim() // Do something here with the trimmed email }
Understanding Groovy's Null-Safe Comparison Operators
It's important to know that Groovy's comparison operators ==
and !=
handle nulls gracefully so you don't have to worry about protecting null
values in equality or inequality comparisons. Furthermore, the >
, >=
, <
, and <=
operators are also designed to avoid null-related exceptions, however you need to be conscious of how Groovy treats null
in these order-dependent comparisons. Effectively, a null
value is "less than" any other non-null value in the natural ordering, so for example observe the following comparison results.
Table Examples of How null Is Less Than Everything
Left-Side Expression |
Operator |
Right-Side Expression |
Comparison Result |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
If you want a comparison to treat a null-valued field with different semantics — for example, treating a null MaximumOverdraftAmount
field as if it were zero (0) like a spreadsheet user might expect — then use the nvl()
function as part of your comparison logic as shown in the following example:
// Change default comparison semantics for the MaximumOverdraftAmount custom field in // case its value is null by using nvl() to treat null like zero (0) if (nvl(MaximumOverdraftAmount,0) < -2000) { // do something for suspiciously large overdraft amount }
As illustrated by the table above, without the nvl()
function in the comparison any MaximumOverdraftAmount
value of null
would always be less than -2000
— since by default null
is less than everything.
Using Functions as Objects with Closures
While writing helper code for your application, you may find it handy to treat a function as an object called a closure. It lets you to define a function you can store in a variable, accept as a function parameter, pass into another function as an argument, and later invoke on-demand, passing appropriate arguments as needed.
Order
object's
computeTaxForOrder()
function shown below declares a
taxStrategyFunction
parameter of type
Closure
to accept a tax strategy function from the caller. At an appropriate place in the code, it invokes the function passed-in by applying parentheses to the parameter name, passing along any arguments.
// Object function on Order object // Float computeTaxForOrder(Closure taxStrategyFunction) Float totalTax = 0 // Iterate over order line items and return tax using // taxStrategyFunction closure passed in def orderLines = orderLinesCollection orderLines.reset() while (orderLines.hasNext()) { // Invoke taxStrategyFunction() passing current line's lineTotal def currentLine = orderLines.next() totalTax += taxStrategyFunction(currentLine.lineTotal) } return totalTax
In one territory ABC, imagine that amounts under 25 euros pay 10% tax while items 25 euros or over pay 22%. In a second territory DEF, sales tax is a flat 20%. We could represent these two tax computation strategies as separate function variables as shown below. The closure is a function body enclosed by curly braces that has no explicit function name. By default the closure function body accepts a single parameter named it
that will evaluate to null
if no parameter is passed at all when invoked. Here we've saved one function body in the variable named taxForTerritoryABC
and another in the variable taxForTerritoryDEF
.
def taxForTerritoryABC = { return it * (it < 25 ? 0.10 : 0.22) } def taxForTerritoryDEF = { return it * 0.20 }
return
keyword as shown below, since Groovy returns the last evaluated expression as the function return value if not explicitly returned using the
return
statement.
def taxForTerritoryABC = { it * (it < 25 ? 0.10 : 0.22) } def taxForTerritoryDEF = { it * 0.20 }
Order
object's
computeTaxForOrder()
as shown below. Here we're calling it from a
Before Insert trigger on the
Order
object:
// Before Insert trigger on Order def taxForTerritoryABC = { it * (it < 25 ? 0.10 : 0.22) } // Assign the value of totalTax field, using the taxForTerritoryABC // function to compute the tax for each line item of the order. totalTax = computeTaxForOrder(taxForTerritoryABC)
If you don't like the default name it
for the implicit parameter passed to the function, you can give the parameter an explicit name you prefer using the following "arrow" (->
) syntax. The parameter name goes on the left, and the body of the function on the right of the arrow:
def taxForTerritoryABC = { amount -> amount * (amount < 25 ? 0.10 : 0.22) } def taxForTerritoryDEF = { val -> val * 0.20 }
The closure is not limited to a single parameter. Consider the following slightly different tax computation function on the Order
object named computeTaxForOrderInCountry()
. It accepts a taxStrategyFunction
that it invokes with two arguments: an amount to be taxed and a country code.
// Object function on Order object // BigDecimal computeTaxForOrderInCountry(Closure taxStrategyFunction) { BigDecimal totalTax = 0 // Iterate over order line items and return tax using // taxStrategyFunction closure passed in def orderLines = orderLinesCollection orderLines.reset() while (orderLines.hasNext()) { // Invoke taxStrategyFunction() passing current line's lineTotal // and the countryCode field value from the owning Order object def currentLine = orderLines.next() totalTax += taxStrategyFunction(currentLine.lineTotal, currentLine.order.countryCode) } return totalTax
computeTaxForOrderInCountry
must declare
both parameters and give each a name as shown in the example below. Notice that the function body can contain multiple lines if needed.
def taxForTerritoryABC = { amount, countryCode -> if (countryCode == 'IT') { return amount * (amount < 25 ? 0.10 : 0.22) } else { return amount * (amount < 50 ? 0.12 : 0.25) } }
// Before Insert trigger on Order: Assign totalTax // using a flat 0.22 tax regardless of countryCode totalTax = computeTaxForOrderInCountry( { amount, country -> return 0.22 } )
totalTax = computeTaxForOrderInCountry{ amount, country -> return 0.22 }
findAll()
function shown below finds all email addresses in the list that end with the
.edu
suffix.
def recipients = ['sjc@example.edu','dan@example.com', 'spm@example.edu','jim@example.org'] def eduAddreses = recipients.findAll{ it?.endsWith('.edu') }
def logCurrentTime = { -> println("Current time is ${now()}") }
// Invoke the closure's function body with no arguments logCurrentTime()
// This will FAIL because the closure demands no arguments! logCurrentTime(123)
Working More Cleverly with Collections
Business logic frequently requires working with collections of values. This section explains the most useful functions you can use to work with your collections to keep code clean, readable, and easy to understand.
Finding Items in a Collection
To find all items matching a condition in a collection, use the findAll()
function. It accepts a boolean closure identifying the items you're looking for. The result is a List
of all items in the collection for which the closure evaluates to true
. If no item matches or the collection is empty, then an empty collection is returned.
.edu
suffix:
def recipients = ['sjc@example.edu','dan@example.com', 'spm@example.edu','jim@example.org'] // Pass boolean closure using implicit "it" parameter with find criteria // (using safe-navigation operator in case any element is null) def eduAddreses = recipients.findAll{ it?.endsWith('.edu') }
When applied to a List
of Map
objects, your closure can reference the current map's keys by name as shown below. This example produces a list of phonebook entries having a phone number that starts with the country code "+39-" for Italy.
def phonebook = [ [name: 'Steve', phone: '+39-123456789'], [name: 'Joey', phone: '+1-234567890'], [name: 'Sara', phone: '+39-345678901'], [name: 'Zoe', phone: '+44-456789123'] ] def italianFriends = phonebook.findAll { it?.phone?.startsWith('+39-') }
findAll()
on a
Map
, then the parameter passed to the closure on each evaluation is the current
Map
entry. Each entry has a
key
and
value
property you can reference in the closure function body if necessary. The result is a
Map
containing only the entries for which the closure evaluates to true. In the example below, the result is a map containing the two users' map entries whose
name
is
Steve
.
def users = [ 'smuench':[name:'Steve', badge:'A123'], 'jevans':[name:'Joe', badge:'B456'], 'sburns':[name:'Steve', badge:'C789'] ] def usersNamedSteve = users.findAll { it?.value.name == 'Steve' }
find()
function instead of
findAll()
. It accepts the same boolean closure but stops when the first match is identified. Note that in contrast to
findAll()
, when using
find()
if no item matches the predicate or the collection was empty to begin with then
null
is returned.
Companion functions exist to perform other searching operations like:
any { boolean_predicate }
— returns true if boolean_predicate returnstrue
for any itemevery { boolean_predicate }
— returns true if boolean_predicate returnstrue
for every item
Generating One Collection from Another
collect()
function to produce a new collection from an existing collection. The resulting one contains the results of evaluating a closure for each element in the original collection. In the example below, the
uppercasedNames
collection is a list of the uppercase
name
property values of all the map entries in the phonebook.
def phonebook = [ [name: 'Steve', phone: '+39-123456789'], [name: 'Joey', phone: '+1-234567890'], [name: 'Sara', phone: '+39-345678901'], [name: 'Zoe', phone: '+44-456789123'] ] def uppercasedNames = phonebook.collect { it?.name?.toUpperCase() }
name
property of
phonebook
entries with an Italian phone number.
// First filter phonebook collection, then collect the name values def italianNames = phonebook.findAll { it?.phone?.startsWith('+39-') } .collect { it?.name }
Sorting Items in a Collections
sort()
function. If the collection is a simple list then its items will be sorted ascending by their natural ordering. For example, this line will sort the list of names in alphabetical order. The collection you invoke it on is updated to reflect the sorted ordering:
def names = ['Zane','Jasmine','Abigail','Adam'] names.sort()
users
collection based on the number of
accesses
a user has made.
def users = [ [userid:'smuench', name:'Steve', badge:'A123', accesses: 135], [userid:'jevans', name:'Joe', badge:'B456', accesses: 1001], [userid:'sburns', name:'Steve', badge:'C789', accesses: 52] ] // Sort the list of maps based on the accesses property of each map users.sort { it.accesses }
value
property of the map entry before referencing its
accesses
property as shown here.
def users = [ 'smuench':[name:'Steve', badge:'A123', accesses: 135], 'jevans':[name:'Joe', badge:'B456', accesses: 1001], 'sburns':[name:'Steve', badge:'C789', accesses: 52] ] // Sort the map of maps based on the accesses property of map entry's value users.sort { it.value.accesses }
0
— if they are equal-1
— if the first parameter is less than the second parameter1
— if the first parameter is greater than the second parameter
<=>
). In the example below, the two-parameter closure uses this operator to return the appropriate integer based on comparing the value of the
accesses
property of the the first map entry's value with the corresponding value of the same property on the second map entry's value.
// Sort map of maps by comparing the accesses property of map entry's value users.sort { a, b -> a.value.accesses <=> b.value.accesses }
To reverse the sort order to be descending if needed, simply swap the roles of the two parameters passed to the closure. For example, to sort the user list descending by number of accesses, as shown below, swap the a
and b
parameters on the right side of the arrow:
// Sort map of maps DESCENDING by comparing the accesses property of map entry's value users.sort { a, b -> b.value.accesses <=> a.value.accesses }
If your sorting needs are more complex, you can implement the comparator closure in any way you need to, so long as it returns one of the three expected integer values.
Grouping Items in a Collection
groupBy()
function, providing a closure to evaluate as the grouping key. For example, given a list of words you can group them based on the length of each word by doing the following:
def words = ['For', 'example', 'given', 'a', 'list', 'of', 'words', 'you', 'can', 'group', 'them', 'based', 'on', 'the', 'length', 'of', 'each', 'word'] def groupedByLength = words.groupBy{ it.length() }
Map
of
List
:
[ 3:['For', 'you', 'can', 'the'], 7:['example'], 5:['given', 'words', 'group', 'based'], 1:['a'], 4:['list', 'them', 'each', 'word'], 2:['of', 'on', 'of'], 6:['length'] ]
countBy()
function, passing the same kind of closure to determine the grouping key:
def countsByLength = words.countBy{ it.length() }
This produces a map with the word lengths as the map key and the count as the value:
[3:4, 7:1, 5:4, 1:1, 4:4, 2:3, 6:1]
You can group and sort any collection as needed. For example, after grouping and counting the list of words above, you can group the resulting map into further groups based on whether the words have an even number of characters or an odd number of characters like this:
def evenOdd = countsByLength.groupBy{ it.key % 2 == 0 ? 'even' : 'odd' }
[odd:[3:4, 7:1, 5:4, 1:1], even:[4:4, 2:3, 6:1]]
def shortWordCounts = words.findAll{ it.length() < 3 } .countBy{ it } .sort{ it.key }
def shortWordCounts = words.findAll{ word -> word.length() < 3 } .countBy{ word -> word .sort{ wordCountMapEntry -> wordCountMapEntry.key }
def shortWordCounts = // Find words less than 3 characters words.findAll{ word -> word.length() < 3 } // Then count how many times each resulting word occurs .countBy{ word -> word } // Then sort alphabetically by word .sort{ wordCountMapEntry -> wordCountMapEntry.key }
[a:1, of:2, on:1]
Computing Aggregates Over a Collection
You can easily compute the count, sum, minimum, or maximum of items in a collection. This section describes how to use these four collection functions.
Computing the Count of Items in a Collection
size()
function. However, if you need to count a subset of items in a collection based on a particular condition, then use
count()
. If you provide a single value, it returns a count of occurrences of that value in the collection. For example, the following use of
count('bbb')
returns the number 2.
def list = ['aa','bbb','cccc','defgh','bbb','aa','defgh','defgh'] // If there are two or more 'bbb' then do something... if (list.count('bbb') >= 2){ /* etc. */ }
count()
function also accepts a boolean closure identifying which items to count. For example, to count the strings in a list whose lengths are an even number of characters, use code like the following. The count reflects the items for which the closure evaluates to
true
.
def list = ['aa','bbb','cccc','defgh','bbb','aa','defgh','defgh'] def numEvenLengths = list.count{ it.length() % 2 == 0 }
countBy()
function. It takes a closure that identifes the grouping key before computing the count of the items in each group. For example, to count the number of occurrences of items in the list above, use:
def entriesAndCounts = list.countBy{ it }
[aa:2, bbb:2, cccc:1, defgh:3]
def entriesAndCounts = list.countBy{ it } .sort{ a, b -> b.value <=> a.value }
[defgh:3, aa:2, bbb:2, cccc:1]
find()
function that returns the first element.
def topWord = list.countBy{ it } .sort{ a, b -> b.value <=> a.value } .find() println "Top word '${topWord.key}' appeared ${topWord.value} times"
Computing the Minimum of Items in a Collection
min()
function with no arguments. However, if you need to find the minimum from a subset of items in a collection based on a particular condition, then pass a closure to
min()
that identifies the expression for which to find the minimum value. For example, to find the minimum item in the following list of users based on the number of accesses they've made to a system, do the following:
def users = [ 'smuench':[name:'Steve', badge:'A123', accesses: 135], 'sburns':[name:'Steve', badge:'C789', accesses: 52], 'qbronson':[name:'Quello', badge:'Z231', accesses: 52], 'jevans':[name:'Joe', badge:'B456', accesses: 1001] ] // Return the map entry with the minimum value based on accesses def minUser = users.min { it.value.accesses }
min()
function returns the
first item having the minimum
accesses
value of 52, which is the map entry corresponding to
sburns
. However, to return all users having the minimum value requires first determining the minimum value of accesses and then finding all map entries having that value for their
accesses
property. This code looks like:
// Find the minimum value of the accesses property def minAccesses = users.min { it.value.accesses }.value.accesses // Return all map entries having that value for accesses def usersWithMinAccesses = users.findAll{ it.value.accesses == minAccesses }
There is often more than one way to solve a problem. Another way to compute the minimum number of accesses would be to first collect()
all the accesses
values, then call min()
on that collection of numbers. That alternative approach looks like this:
// Find the minimum value of the accesses property def minAccesses = users.collect{ it.value.accesses }.min()
[ sburns:[name:'Steve', badge:'C789', accesses:52], qbronson:[name:'Quello', badge:'Z231', accesses:52] ]
If the collection whose minimum item you seek requires a custom comparison to be done correctly, then you can pass the same kind of two-parameter comparator closure that the sort()
function supports.
Computing the Maximum of Items in a Collection
max()
function with no arguments. However, if you need to find the maximum from a subset of items in a collection based on a particular condition, then pass a closure to
max()
that identifies the expression for which to find the maximum value. For example, to find the maximum item in the following list of users based on the number of accesses they've made to a system, do the following:
def users = [ 'smuench':[name:'Steve', badge:'A123', accesses: 1001], 'sburns':[name:'Steve', badge:'C789', accesses: 52], 'qbronson':[name:'Quello', badge:'Z231', accesses: 152], 'jevans':[name:'Joe', badge:'B456', accesses: 1001] ] // Return the map entry with the maximum value based on accesses def maxUser = users.max { it.value.accesses }
max()
function returns the
first item having the maximum
accesses
value of 1001, which is the map entry corresponding to
smuench
. However, to return all users having the maximum value requires first determining the maximum value of accesses and then finding all map entries having that value for their
accesses
property. This code looks like:
// Find the maximum value of the accesses property def maxAccesses = users.max { it.value.accesses }.value.accesses // Return all map entries having that value for accesses def usersWithMaxAccesses = users.findAll{ it.value.accesses == maxAccesses }
There is often more than one way to solve a problem. Another way to compute the maximum number of accesses would be to first collect()
all the accesses
values, then call max()
on that collection of numbers. That alternative approach looks like this:
// Find the maximum value of the accesses property def maxAccesses = users.collect{ it.value.accesses }.max()
[ smuench:[name:Steve, badge:A123, accesses:1001], jevans:[name:Joe, badge:B456, accesses:1001] ]
If the collection whose maximum element you seek requires a custom comparison to be done correctly, then you can pass the same kind of two-parameter comparator closure that the sort()
function supports.
Computing the Sum of Items in a Collection
To determine the sum of items in a collection call its sum()
function with no arguments. This works for any items that support a plus operator. For example, you can sum a list of numbers like this to produce the result 1259.13:
def salaries = [123.45, 678.90, 456.78] // Compute the sum of the list of salaries def total = salaries.sum()
VincentvanGogh
:
def names = ['Vincent','van','Gogh'] def sumOfNames = names.sum()
findAll()
to identify the subset you want to consider, then
collect()
the value you want to sum, then finally call
sum()
on that collection. For example, to find the sum of all accesses for all users with over 100 accesses, do the following to compute the total of 2154:
def users = [ 'smuench':[name:'Steve', badge:'A123', accesses: 1001], 'sburns':[name:'Steve', badge:'C789', accesses: 52], 'qbronson':[name:'Quello', badge:'Z231', accesses: 152], 'jevans':[name:'Joe', badge:'B456', accesses: 1001] ] // Compute sum of all user accesses for users having more than 100 accesses def total = users.findAll{ it.value.accesses > 100 } .collect{ it.value.accesses } .sum()
Joining Items in a Collection
String
, use its
join()
function as shown below, passing the string you want to be used as the separator between list items.
def paths = ['/bin', '/usr/bin', '/usr/local/bin'] // Join the paths in the list, separating by a colon def pathString = recipients.join(':')
/bin:/usr/bin:/usr/local/bin
Using Optional Method Arguments
Using optional, named method arguments on your helper functions can make your code easier to read and more self-documenting. For example, consider a object helper function queryRows()
that simplifies common querying use cases. Sometimes your calling code only requires a select
list and a from
clause:
def rates = queryRows(select: 'fromCurrency,toCurrency,exchangeRate', from: 'DailyRates')
where
clause to filter the data and an
orderBy
parameter to sort it:
def euroRates = queryRows(select: 'fromCurrency,toCurrency,exchangeRate', from: 'DailyRates', where: "fromCurrency = 'EUR'", orderBy: 'exchangeRate desc')By using optional, named arguments, your calling code specifies only the information required and clarifies the meaning of each argument. To adopt this approach, use a single parameter of type
Map
when defining your function:
// Global Function List queryRows(Map options)
queryRows()
function is to explicitly pass a
Map
as its single argument like this:
// Passing a literal Map as the first argument of queryRows() def args = [select: 'fromCurrency,toCurrency,exchangeRate', from: 'DailyRates'] def rates = queryRows(args)
You can also pass a literal Map
inline without assigning it to a local variable like this:
// Passing a literal Map inline as the first argument of queryRows() def rates = queryRows([select: 'fromCurrency,toCurrency,exchangeRate', from: 'DailyRates'])
Map
directly inside the function call argument list you can omit the square brackets. This makes the code easier to read:
// Passing a literal Map inline as the first argument of queryRows() // In this case, Groovy allows removing the square brackets def rates = queryRows(select: 'fromCurrency,toCurrency,exchangeRate', from: 'DailyRates')
Map
argument representing your function's optional parameters must be first. If your function defines
additional parameters, then when calling the function, pass the values of the other parameters first
followed by any optional, named parameters you want to include. For example, consider the signature of following
findMatchingOccurrences()
object function that returns the number of strings in a list that match a search string. The function supports three optional
boolean
parameters
caseSensitive
,
expandTokens
,
useRegExp
.
Long findMatchingOccurrences(Map options, List stringsToSearch, String searchFor)
stringsToSearch
and
searchFor
as shown below:
// Use an object function to count how many emails // are from .org or .edu sites def nonCommercial = findMatchingOccurrences(emails,'.*.org|.*.edu', caseSensitive: true, useRegExp: true)
Regardless of the approach the caller used to pass in the key/value pairs, your function body works with optional, named arguments as entries in the leading Map
parameter. Be aware that if no optional argument is included, then the leading Map
parameter evaluates to null
. So assume the options
parameter might be null
and handle that case appropriately.
queryRows()
global function. Notice it uses the safe-navigation operator (
?.
) when referencing the
select
property of the
options
parameter just in case it might be
null
and signals an error using another global function named
error()
.
// Object Function: List queryRows( Map options ) // --------------- // The options Map might be null if caller passes no named parameters // so check uses the safe-navigation operator to gracefully handle the // options == null case, too. We're assuming another object helper function // named 'error()' exists to help throw exception messages. if (!options?.select) { error("Must specify list of field names in 'select' parameter") } if (!options?.from) { error("Must specify object name in 'from' parameter") } // From here, we know that some options were supplied, so we do not // need to continue using the "?." operator when using options.someName def vo = newView(options.from) // etc.
Creating a New Object
To create a new object, follow these steps:
Use the
newView()
function to obtain the view object for programmatic access for the business object in questionCall the
createRow()
function on the view object to create a new rowSet the desired field values in the new row
Call
insertRow()
on the view object to insert the row.
The new object will be saved the next time you save your work as part of the current transaction. The example below shows how the steps fit together in practice.
// Access the view object for the custom TroubleTicket object def vo = newView('TroubleTicket') // Create the new row def newTicket = vo.createRow() // Set the problem summary newTicket.ProblemSummary = 'Cannot insert floppy disk' // Assign the ticket a priority newTicket.Priority = 2 // Insert the new row into the view object vo.insertRow(newTicket) // The new data will be saved to the database as part of the current // transaction when it is committed.
Updating an Existing Object
If the object you want to update is the current row in which your script is executing, then just assign new values to the fields as needed.
However, if you need to update an object that is different from the current row, perform these steps:
Use
newView()
to access the appropriate view object for programmatic accessFind the object by id or find one or more objects using a view criteria, depending on your requirements
Assign new values to fields on this row as needed
The changes will be saved as part of the current transaction when the user commits it.
Permanently Removing an Existing Object
To permanently remove an existing object, perform these steps:
Use
newView()
to access the appropriate view object for programmatic accessFind the object by id or find one or more objects using a view criteria, depending on your requirements
Call the
remove()
method on the row or rows as needed
The changes will be saved as part of the current transaction when the user commits it.
Reverting Changes in a Single Row
To revert pending changes to an existing object, perform these steps:
Use
newView()
to access the appropriate view object for programmatic accessFind the object by id
Call the
revertRowAndContainees()
method as follows on the rowyourRow.revertRowAndContainees()
Understanding Why Using Commit or Rollback In Scripts Is Strongly Discouraged
By design you cannot commit or rollback the transaction from within your scripts. Any changes made by your scripts get committed or rolled-back along with the rest of the current transaction. If your script code were allowed to call commit()
or rollback()
, this would affect all changes pending in the current transaction, not only those performed by your script and could lead to data inconsistencies.
Using the User Data Map
The application development framework provides a map of name/value pairs that is associated with the current user's session. You can use this map to temporarily save name/value pairs for use by your business logic. Be aware that the information that you put in the user data map is never written out to a permanent store, so values your code puts into the user data map are only available during the current request.
To access the server map from a validation rule or trigger, use the expression adf.userSession.userData
as shown in the following example:
// Put a name/value pair in the user data map adf.userSession.userData.put('SomeKey', someValue) // Get a value by key from the user data map def val = adf.userSession.userData.SomeKey
Referencing Information About the Current User
The adf.context.getSecurityContext()
expression provides access to the security context, from which you can access information about the current user like her user name or whether she belongs to a particular role. The following code illustrates how to reference these two pieces of information:
// Get the security context def secCtx = adf.context.getSecurityContext() // Check if user has a given role if (secCtx.isUserInRole('MyAppRole')) { // get the current user's name def user = secCtx.getUserName() // Do something if user belongs to MyAppRole }
Using Aggregate Functions
Built-in support for row iterator aggregate functions can simplify a number of common calculations you will perform in your scripts, especially in the context of scripts written in a parent object which has one or more collections of child objects.
Understanding the Supported Aggregate Functions
Five built-in aggregate functions allow summarizing rows in a row set. The most common use case is to calculate an aggregate value of a child collection in the context of a parent object. The table below provides a description and example of the supported functions.
Table Supported Aggregate Functions
Aggregate Function |
Description |
Example (in Context of |
---|---|---|
|
Average value of an expression |
|
|
Minimum value of an expression |
|
|
Maximum value of an expression |
|
|
Sum of the value of an expression |
|
|
Count of rows having a non-null expression value |
|
Understanding Why Aggegrate Functions Are Appropriate Only to Small Numbers of Child Rows
The aggregate functions described in this section compute their result by retrieving the rows of a child collection from the database and iterating through all of these rows in memory. This fact has two important consequences. The first is that these aggregate functions should only be used when you know the number of rows in the child collection will be reasonably small. The second is that your calculation may encounter a runtime error related to exceeding a fetch limit if the child collection's query retrieves more than 500 rows.
Understanding How Null Values Behave in Aggregate Calculation
When an ADF aggregate function executes, it iterates over each row in the row set. For each row, it evaluates the Groovy expression provided as an argument to the function in the context of the current row. If you want a null value to be considered as zero for the purposes of the aggregate calculation, then use the nvl()
function like this:
// Use nvl() Function in aggregate expression def avgDuration = ActivityCollection.min('nvl(Duration,0)')
Performing Conditional Counting
In the case of the count()
function, if the expression evaluates to null then the row is not counted. You can supply a conditional expression to the count()
function which will count only the rows where the expression returns a non-null value. For example, to count the number of child activities for the current trouble-ticket where the Duration was over half an hour, you can use the following expression:
// Conditional expression returns non-null for rows to count // Use the inline if/then/else operator to return 1 if the // duration is over 0.5 hours, otherwise return null to avoid // counting that the non-qualifying row. def overHalfHourCount = ActivityCollection.count('nvl(Duration,0) > 0.5 ? 1 : null')
Understanding the Difference Between Default Expression and Create Trigger
There are two ways you can assign default values to fields in a newly-created row and it is important to understand the difference between them.
The first way is to provide a default value expression for one or more fields in your object. Your default value expression should not depend on other fields in the same object since you cannot be certain of the order in which the fields are assigned their default values. The default value expression should evaluate to a legal value for the field in question and it should not contain any field assignments or any setAttribute()
calls as part of the expression. The framework evaluates your default expression and assigns it to the field to which it is associated automatically at row creation time.
On the other hand, If you need to assign default values to one or more fields after first allowing the framework to assign each field's literal default values or default value expression, then the second way is more appropriate. Define a Create
trigger on the object and inside that trigger you can reference any field in the object as well as perform any field assignments or setAttribute()
calls to assign default values to one or more fields.
Deriving Values of a Field When Other Fields Change Value
There are three different use cases where you might want to derive the value of a field. This section assists you in determining which one is appropriate for your needs.
Deriving the Value of a Formula Field When Other Fields Change Value
If the value of your derived field is calculated based on other fields and its calculated value does not need to be permanently stored, then use a formula field. To derive the value of the formula field, perform these two steps:
Configure the field’s Value Calculation setting to Calculate value with a formula
Enter the formula as Groovy script that returns a value compatible with the field’s type
Deriving the Value of Non-Formula Field When Other Fields Change Value
If the value of your derived field must be stored, then use one of strategies in this section to derive its value.
Deriving a Non-Formula Field Using a Before Trigger
If your derived value depends on multiple fields, or you prefer to write all field derivation logic in a single trigger, then create an appropriate "before" trigger (Before Insert and/or Before Update) that computes the derived values and assigns each to its respective field. See Testing Whether a Field's Value Is Changed for more information on this function and Avoiding Validation Threshold Errors By Conditionally Assigning Values for a tip about how to avoid your field assignments from causing an object to hit its validation threshold.
Deriving a Non-Formula Field Using an After Field Changed Trigger
If your derived value depends on a single field’s value, then consider writing an After Field Changed trigger. When this trigger fires, the value of the field in question has already changed. Therefore, you can simply reference the new value of the field by name instead of using the special newValue
expression (as would be required in a field-level validation rule to reference the field's candidate new value that is attempting to be set).
Setting Invalid Fields for the UI in an Object-Level Validation Rule
When a field-level validation rule that you've written returns false
, ADF signals the failed validation with an error and the field is highlighted in the user interface to call the problem to the user's attention. However, since object-level validation rules involve multiple fields, the framework does not know which field to highlight in the user interface as having the problematic value. If you want your object-level validation rule to highlight one or more fields as being in need of user review to resolve the validation error, you need to assist the framework in this process. You do this by adding a call to the adf.error.addAttribute()
function in your validation rule script before returning false
to signal the failure. For example, consider the following rule to enforce: A contact cannot be his/her own manager. Since the id
field of the Contact
object cannot be changed, it will make sense to flag the manager
reference field as the field in error to highlight in the user interface. Here is the example validation rule.
Rule Name:
Contact_Cannot_Be_Own_Manager
Error Message:
A contact cannot be his/her own manager
Rule Body
// Rule depends on two fields, so must be // written as object-level rule if (manager == id) { // Signal to highlight the Manager field on the UI // as being in error. Note that Manager_Id field // is not shown in the user interface! adf.error.addAttribute('manager') return false } return true
Determining the State of a Row
A row of data can be in any one of the following states:
New
A new row that will be inserted into the database during the next save operation.
Unmodified
An existing row that has not been modified
Modified
An existing row where one or more values has been changed and will be updated in the database during the next save operation
Deleted
An existing row that will be deleted from the database during the next save operation
Dead
A row that was new and got removed before being saved, or a deleted row after it has been saved
To determine the state of a row in your Groovy scripts, use the function getPrimaryRowState()
and its related helper methods as shown in the following example.
// Only perform this business logic if the row is new if (getPrimaryRowState().isNew()) { // conditional logic here }
The complete list of helper methods that you can use on the return value of getPrimaryRowState()
is shown below:
isNew()
Returns boolean
true
if the row state is new,false
otherwise.isUnmodified()
Returns boolean
true
if the row state is unmodified,false
otherwise.isModified()
Returns boolean
true
if the row state is modified,false
otherwise.isDeleted()
Returns boolean
true
if the row state is deleted,false
otherwise.isDead()
Returns boolean
true
if the row state is dead,false
otherwise.
Understanding How Local Variables Hide Object Fields
If you define a local variable whose name is the same as the name of a field in your object, then be aware that this local variable will take precedence over the current object's field name when evaluated in your script. For example, assuming an object has a field named Status
, then consider the following object validation script:
// Assuming current object has a Status field, define local variable of the same name def Status = 'Closed' /* * : * Imagine pages full of complex code here * : */ // If the object's current status is Open, then change it to 'Pending' // ------------------ // POTENTIAL BUG HERE: The Status local variable takes precedence // ------------------ so the Status field value is not used! // if (Status == 'Open') { Status = 'Pending' }
At the top of the example, a variable named Status
is defined. After pages full of complex code, later in the script the author references the custom field named Status
without remembering that there is also a local variable named Status
defined above. Since the local variable named Status
will always take precedence, the script will never enter into the conditional block here, regardless of the current value of the Status
field in the current object. As a rule of thumb, use a naming scheme for your local variables to ensure their names never clash with object field names.
Invoking REST Services from Your Scripts
Calling a REST service endpoint from your scripts involves these high-level steps:
Create a service connection with one or more endpoints, choosing meaningful Service Id and Endpoint Ids
Write Groovy code to:
Acquire a new service object using
newService(’yourServiceId’)
Set any necessary path parameters, query parameters, or HTTP header fields
Construct a payload object if one is required
Invoke an endpoint method on the service object, passing a payload object if needed
If successful, process the response payload, checking HTTP status code if needed
If exception was thrown, catch and handle it, checking HTTP status code if needed
This section explains these steps in more detail.
Creating a Service Connection
To invoke a REST service from your Groovy script, start by creating a service connection. Visual Builder defaults a value for its Service Name and Service Id fields, but allows you to change both if desired. Since your code will reference the service using its Service Id, choose an identifier that will help others reading your code understand what the service does. Note that the value of Service Id cannot be changed after the service connection is created.
Each service consists of one or more endpoints. Each of a service’s endpoints is a distinct operation that the service can perform. Visual Builder defaults a value for each endpoint’s Endpoint Id, but allows you to subsequently change it when you edit the endpoint. Your Groovy code will directly reference the Endpoint Id as a method name when you invoke the service, so choose an identifier that will help others reading your code understand the function each method performs.
UsersService
and a
Service Id of
usersService
. Suppose it has endpoints with
Endpoint Id values
getUser
,
getUsers
,
createUser
, and
updateUser
. Your Groovy code will use the respective
id values of the service and endpoint to call a service operation like
getUser
at runtime. For example, you might write:
def userSvc = newService('usersService') // NOTE: Service Id, not Service Name def newUser = userSvc.createUser([id:456, name:'Steve'])
Acquiring a New Service Object
Every service call you make in a script requires a service object. To obtain an appropriate service object on which to invoke an endpoint method, use the newService()
function, passing in the service id representing the service you want to use. Consider a service connection with a Service Name of UsersService
and a Service Id of usersService
. Use the id value of the service to acquire an appropriate service object:
// NOTE: Service Id, not Service Name def userSvc = newService('usersService')
Setting Path Parameters If Needed
A service connection encapsulates a base URL. For example, a service named UsersService
might correspond to a base URL of https://hcm.example.org
. Each service endpoint, in turn, corresponds to a relative path that complements the service’s base URL to define a unique resource. For example, one of this example service’s endpoints might have an id of getUsers
and correspond to the /users
path. In this case, calling the service may require just two lines of code:
// Get the list of users def userSvc = newService('usersService') def userList = userSvc.getUsers()
Sometimes an endpoint’s path includes a substitution parameter whose value represents the unique id of a resource, like the id of a user in this example. Consider another endpoint named getUser
with /users/{userid}
as its path. The {userid}
represents a path parameter named userid
whose value your code must supply before calling the endpoint method. A failure to do so will result in a runtime error. To set a path parameter named userid
, use the service object’s pathParams
map as shown in this example:
// Get information for the user 3037
def userSvc = newService('usersService')
userSvc.pathParams.userid = '3037'
def user = userSvc.getUser()
If the name of a path parameter is not a legal Groovy identifier because it contains a character like a hyphen or space (e.g. user-id
), use this alternative map syntax instead:
userSvc.pathParams['user-id'] = '3037'
If you set multiple path parameters in a single line, then the user-id
map key still need to be quoted like this:
userSvc.pathParams = ['user-id': '3037', anotherParam: 'anotherValue']
A service endpoint can also have more than one path parameter. In this case, you can set each path parameter separately like this:
// Get information for the user 3037
def userSvc = newService('usersService')
userSvc.pathParams.userid = '3037'
userSvc.pathParams.anotherParam = 'anotherValue'
def user = userSvc.getUser()
Alternatively, you can set all path parameters at the same time by assigning the pathParams
map like this:
// Get information for the user 3037
def userSvc = newService('usersService')
userSvc.pathParams = [userid: '3037', anotherParam: 'anotherValue']
def user = userSvc.getUser()
Setting Query Parameters If Needed
In addition to path parameters, a service endpoint may require supplying one or more values for so-called “query” parameters. These parameters can affect how the service responds to your request, or may even be mandatory. For example, suppose the getUser
endpoint supports a query parameter named format
whose valid values are compact
and verbose
, and a query parameter named currency
to specify the three-letter code of the currency in which to report the user’s balance. The service documentation will clarify whether the format
and currency
parameters are required or optional, and explain any relevant default behavior. Failure to supply a value for a required query parameter may result in a runtime error. To use this endpoint to retrieve the compact form of a given user’s information for the euro currency, use the service object’s queryParams
map as shown in this example:
// Get compact info for the user 3037
def userSvc = newService('usersService')
userSvc.pathParams.userid = '3037'
userSvc.queryParams.format = 'compact'
userSvc.queryParams.currency = 'EUR'
def user = userSvc.getUser()
When setting multiple query parameters, as an alternative approach to setting each parameter on its own line as shown above, it’s also possible to assign the entire parameters map in a single assignment. The example below is equivalent to the one above, but notice the queryParams
map is set to a map containing two entries in a single line:
// Get compact info for the user 3037 def userSvc = newService('usersService') userSvc.pathParams.userid = '3037' userSvc.queryParams = [format: 'compact', currency: 'EUR'] def user = userSvc.getUser()
If the name of a query parameter is not a legal Groovy identifier because it contains a character like a hyphen or space (e.g. base-currency
), use this alternative map syntax instead:
userSvc.queryParams['base-currency'] = 'EUR'
If you are assigning the entire map at once, then it is still required to quote the key value, but the syntax looks like this:
userSvc.queryParams = [format: 'compact', 'base-currency':'EUR']
Setting HTTP Headers If Needed
Beyond using path parameters, and query parameters, a service endpoint might support behavior controlled by header fields. Each header field is a name/value pair where the value is a list containing one or more values. Suppose the documentation for our getUser
service explains that it supports retrieving user information in either XML or JSON format, based on the value of an HTTP header field named Accept
. Assume the two values it recognizes for this field are either application/xml
or application/json
. To retrieve the verbose form of a given user’s information in JSON format, use the service object’s requestHTTPHeaders
map as shown in this example:
// Get compact info for the user 3037
def userSvc = newService('usersService')
userSvc.pathParams.userid = '3037'
userSvc.queryParams.format = 'verbose'
// Notice the value is a list containing one string!
userSvc.requestHTTPHeaders.Accept = ['application/json']
def user = userSvc.getUser()
If the name of a header field is not a legal Groovy identifier because it contains a character like a hyphen or space (e.g. Content-Type
), use this alternative map syntax instead:
userSvc.requestHTTPHeaders['Content-Type'] = ['application/json']
Using Groovy Maps and Lists with REST Services
When passing and receiving structured data from a REST service endpoint, a Groovy Map
represents an object and its properties. In fact, maps and lists are all you need to work with service request and response payloads. In particular, you never need to work directly with the JavaScript Object Notation (JSON) string representation of an object because the platform automatically converts between Groovy objects and JSON as necessary.
For example, an Employee
object with properties named Empno
, Ename
, Sal
, and Hiredate
would be represented by a Map
object having four key/value pairs, where the names of the properties are the keys. You can create an empty Map
using the syntax:
def newEmp = [:]
Then, you can add properties to the map using the explicit put()
method like this:
newEmp.put('Empno',1234) newEmp.put('Ename','Sean') newEmp.put('Sal',9876) newEmp.put('Hiredate',date(2013,8,11))
Alternatively, and more conveniently, you can assign and/or update map key/value pairs using a simpler direct assignment notation like this:
newEmp.Empno = 1234 newEmp.Ename = 'Sean' newEmp.Sal = 9876 newEmp.Hiredate = date(2013,8,11)
Finally, you can also create a new map and assign some or all of its properties at once using the constructor syntax:
def newEmp = [Empno : 1234, Ename : 'Sean', Sal : 9876, Hiredate : date(2013,8,11)]
To create a collection of objects you use the Groovy List
object. You can create one object at a time and then create an empty list, and call the list's add()
method to add both objects to the list:
def dependent1 = [Name: 'Dave', BirthYear: 1996] def dependent2 = [Name: 'Jenna', BirthYear: 1999] def listOfDependents = [] listOfDependents.add(dependent1) listOfDependents.add(dependent2)
To save a few steps, the last three lines above can be done in a single line by constructing a new list with the two desired elements in one line like this:
def listOfDependents = [dependent1, dependent2]
You can also create the list of maps in a single go using a combination of list constructor syntax and map constructor syntax:
def listOfDependents = [[Name: 'Dave', BirthYear: 1996], [Name: 'Jenna',BirthYear: 1999]]
If the employee object above had a property named Dependents
that was a list of objects representing dependent children, you can assign the property using the same syntax as shown above (using a list of maps as the value assigned):
newEmp.Dependents = [[Name: 'Dave', BirthYear: 1996], [Name: 'Jenna',BirthYear: 1999]]
Lastly, note that you can also construct a new employee with nested dependents all in one statement by further nesting the constructor syntax:
def newEmp = [Empno : 1234, Ename : 'Sean', Sal : 9876, Hiredate : date(2013,8,11), Dependents : [ [Name: 'Dave', BirthYear: 1996], [Name: 'Jenna', BirthYear: 1999] ] ]
For more information on Maps and Lists, see Working with Lists and Working with Maps
Checking Success Status and Handling Exceptions
Each service endpoint method call can succeed or fail. If a call succeeds, then your script continues normally. If necessary, you can check the exact value of the success status code in the range of 200
-399
using the HTTPStatusCode
field of the service object. If a call fails, an exception named RestConnectionException
is thrown. Use Groovy’s try
/catch
syntax around the service endpoint method invocation to properly handle an eventual error. If you ignore this best practice, the unhandled exception will be reported to your end user, perhaps causing unnecessary confusion or alarm. If necessary, you can check the exact value of the failure status code in the range of 400
–599
by using the statusCode
field of the exception object. The following example shows both of these techniques in practice:
// import the exceptions for later use below import oracle.adf.model.connection.rest.exception.RestConnectionException import oracle.jbo.ValidationException // Fetch user info for an existing user def userSvc = newService('usersService') userSvc.pathParams.userid = '3037' try { def user = userSvc.getUser() def status = userSvc.HTTPStatusCode // perform logic here on success } catch (RestConnectionException rcex) { def status = rcex.statusCode // on failure, handle error here throw new ValidationException('User registry unavailable, try again later.') }
Calling Service Endpoint Methods
For a registered service having id someService
, to call its service endpoint with id someEndpoint
in your script, do the following:
Import
RestConnectionException
andJboException
for error handlingDefine a service object variable
svc
and assign itnewService(’someService’)
Configure
pathParams
,queryParams
, andrequestHTTPHeaders
maps ofsvc
as neededDefine a variable
reqPayload
if needed and assign it a value-
Inside a
try
block...Define a variable
respPayload
to hold the response payloadAssign
svc.someEndpoint(…)
to the variableProcess the response payload after checking
svc.HTTPStatusCode
if needed
Inside the corresponding
catch
block forRestConnectionException
...Handle the error appropriately, after checking
rcex.statusCode
if necessaryReturn false to fail a validation rule or raise a custom exception
// Object function cloneUser( userIdToClone String ) // Imports only needed once at the top of the script import oracle.adf.model.connection.rest.exception.RestConnectionException import oracle.jbo.ValidationException def svc = newService('usersService') svc.pathParams.userid = userIdToClone svc.queryParams['api-key'] = 'vCDDpu3NjiytQF' svc.requestHTTPHeaders['Some-Header'] = ['Somevalue'] // No payload needed for getUser() def cloneUserResponse = svc.getUser() // Diagnostic println visible in the Logs window println("User ${userIdToClone} to clone: "+ "city = ${cloneUserResponse.address.city}, "+ "zip = ${cloneUserResponse.address.zipcode}") def createRequest = [name: userName, address: [ city: cloneUserResponse.address.city, zipcode: cloneUserResponse.address.zipcode ]] // Get new service, else must clear params/headers maps from previous call svc = newService('usersService') try { def newUserResponse = svc.createUser(createRequest) // Assign business object field remoteUserId // with the system-generated user id from response remoteUserId = newUserResponse.id println("Set remoteUserId of new LocalUser to ${newUserResponse.id}") } catch (RestConnectionException rcex) { throw new ValidationException('User registry unavailable, try again later.') }
Browsing Available Service Endpoint Methods
When writing your scripts, as shown in the figure below, the Services tab in the Code Helper palette displays the endpoint methods for all service connections. Each service’s Service Name appears in a title bar in the list. Use the toggle control to the left of each service name to expand or collapse the list of that service’s endpoints. Clicking on the right arrow in the palette margin inserts the appropriate Groovy code to call the service endpoint in question, providing template code where appropriate.

getUser
function as shown in the figure. This would insert the following lines of Groovy code into your script wherever the cursor is positioned in the editor:
def usersService = newService('usersService'); usersService.pathParams['id'] = 'idValue'; // TODO: Change this value def usersServiceGetUser = usersService.getUser();
After doing this, notice the TODO
comment and adjust the example idValue
in quotes to be the appropriate user id value you want to retrieve from the UsersService
service.
Formatting Numbers and Dates Using a Formatter
Groovy provides the Formatter
object that you can use in a text formula expression or anywhere in your scripts that you need for format numbers or dates. The general pattern for using a Formatter
is to first construct a new instance like this, passing the the expression for the current user's locale as an argument:
def fmt = new Formatter(adf.context.locale)
This Formatter
object you've instantiated will generally be used to format a single, non-null value by calling its format()
method like this:
def ret = fmt.format(
formatString
,
arg1
[,
arg2
, ...,
argN
] )
Note that if you call the format()
method of the same Formatter
object multiple times, then the results are concatenated together. To format several distinct values without having their results be concatentated, instantiate a new Formatter
for each call to a format()
method.
The format string can include a set of special characters that indicate how to format each of the supplied arguments. Some simple examples are provided below, however the complete syntax is covered in the documentation for the Formatter class.
Example of Formatting a Number Using a Formatter
To format a number numberVal
as a floating point value with two (2) decimal places and thousands separator you can do:
Double dv = numberVal as Double def fmt = new Formatter(adf.context.locale) def ret = (dv != null) ? fmt.format('%,.2f', dv) : null
If the value of numberVal
were 12345.6789
, and the current user's locale is US English, then this would produce a formatted string like:
12,345.68
If instead the current user's locale is Italian, it would produce a formatted string like:
12.345,68
To format a number numberVal
as a floating point value with three (3) decimal places and no thousands separator you can do:
Double dv = numberVal as Double def fmt = new Formatter(adf.context.locale) def ret = (dv != null) ? fmt.format('%.3f', dv) : null
If the value of numberVal
were 12345.6789
, and the current user's locale is US English, then this would produce a formatted string like:
12345.679
To format a number value with no decimal places to have a zero-padded width of 8, you can do:
Long lv = numberVal as Long def fmt = new Formatter(adf.context.locale) def ret = (lv != null) ? fmt.format('%08d', lv) : null
If the value of numberVal
were 5543
, then this would produce a formatted string like:
00005543
Formatting a Date Using a Formatter
To format a datetime value datetimeVal
to display only the hours and minutes in 24-hour format, you can do:
Date dv = datetimeVal as Date def fmt = new Formatter(adf.context.locale) def ret = (dv != null) ? fmt.format('%tH:%tM', dv, dv) : null
If the value of datetimeVal
were 2014-03-19 17:07:45
, then this would produce a formatted string like:
17:07
To format a date value dateVal
to display the day of the week, month name, the day, and the year, you can do:
Date dv = dateVal as Date def fmt = new Formatter(adf.context.locale) def ret = (dv != null) ? fmt.format('%tA, %tB %te, %tY',dv,dv,dv,dv) : null
If the value of dateVal
were 2014-03-19
, and the current user's locale is US English, then this would produce a formatted string like:
Wednesday, March 19, 2014
Working with Field Values Using a Parameterized Name
When writing reusable code, if your object function needs to perform the same operations on different fields, you can parameterize the field name. Start by defining a function parameter of type String
whose value at runtime will be the name of a field in the current object. Then, when your code needs to access the value of the parameterized field, just call getAttribute(fieldNameParam)
. To assign a new value to that field, call setAttribute(fieldNameParam,newValue)
. In either case, if the value of the field name parameter passed in does not match the name of some field in the current object, a NoDefException
will be thrown to signal an error.
Consider the following example of an object function named conditionalIncrement()
that increments the value of the number field whose name is passed in only if the field’s value is less than a maximum value also passed in:
// Object function: void conditionalIncrement(fieldName String, maxValue Long) // --------------- def fieldValue = getAttribute(fieldName) if (fieldValue < maxValue) { setAttribute(fieldName, fieldValue + 1) }
The first line defines a fieldValue
variable to store the value of the field whose name is passed in. If its value is less than maxValue
, then line three assigns the field a new value that is one greater than its current value. Once you define an object function like conditionalIncrement()
, then any Groovy scripts on the same object can invoke it, passing in appropriate argument values. For example, in one script suppose you need to increment the value of a field named UsageCount
if its value is less than 500:
// Increment the usage count if it is less than 500 conditionalIncrement('UsageCount', 500)
In another script, imagine you need to increment the value of a DocumentVersionNumber
field if its value is less than 1000. You can use the same object function: just pass in different values for the field name and maximum value parameters:
// Increment the document version number if it is less than 1000 conditionalIncrement('DocumentVersionNumber', 1000)
Of course the getAttribute()
and setAttribute()
functions can also accept a literal String
value as their first argument, so you could theoretically write conditional logic like:
// Ensure document is not locked before updating request-for-approval date // NOTE: more verbose get/setAttribute() approach if (getAttribute('DocumentStatus') != 'LOCKED') { setAttribute('RequestForApprovalDate', today()) }
However, in the example above, when the name of the field being evaluated and assigned is not coming from a parameter or local variable, then it is simpler and more readable to write this equivalent code instead:
// Ensure document is not locked before updating request-for-approval date // NOTE: More terse, elegant direct field name access if (DocumentStatus != 'LOCKED') { RequestForApprovalDate = today() }
When invoked on their own, the getAttribute()
and setAttribute()
functions operate on the current object. However, anywhere in your script code where you are working with a business object Row
, you can also call these functions on that particular row as shown in the following example of an object function. Notice that it also parameterizes the name of the object passed to the newView()
function:
// Object function: String getRowDescription(objectName String, displayFieldName String, id Long) // --------------- // Create a new view object to work with the business object whose name is // passed in the objectName parameter def view = newView(objectName) // Find the row in that view whose key is given by the value of the id parameter def rows = view.findByKey(key(id),1) // If we found exactly one row, return the value of the display field name on // that row, whose field name is given by the value in the displayFieldName parameter return rows.size() == 1 ? return rows[0].getAttribute(displayFieldName) : null
With such a function defined, we can invoke it from any script in the object to access the display field value of different objects we might need to work with:
// Get RecordName of the Task object with key 123456 def taskName = getRowDescription('Task','RecordName',123456) // Get the Name of the Territory object with key 987654 def optyName = getRowDescription('Territory','Name',987654)
If you use the getAttribute()
or setAttribute()
to access field values on a related object, remember that the first argument must represent the name of a single field on the object on which you invoke it. For example, the following is not a correct way to use the setAttribute()
function to set the Status
field of the parent TroubleTicket
object for an activity because TroubleTicket?.Status
is not the name of a single field on the current Activity
object:
// Assume script runs in context of an Activity object (child of TroubleTicket) // INCORRECT way to set a parent field's value using setAttribute() setAttribute('TroubleTicket?.Status', 'Open')
Instead, first access the related object and store it in a local variable. Then you can assign a field on the related object as follows:
// Assume script runs in context of an Activity object (child object TroubleTicket) // First access the parent object def parentTicket = TroubleTicket // Then call the setAttribute on that parent object parentTicket?.setAttribute('Status', 'Open')