Difference between revisions of "Java Code Samples"
imported>Aeric |
Wikidevuser (talk | contribs) |
||
(61 intermediate revisions by one other user not shown) | |||
Line 1: | Line 1: | ||
__NUMBEREDHEADINGS__ | <noinclude>__NUMBEREDHEADINGS__ | ||
__TOC__ | __TOC__</noinclude> | ||
==About the Code Samples== | ===About the Code Samples=== | ||
These samples assume basic familiarity with the use of the platform's Java APIs. In many cases, it is also helpful to be conversant with the platform's System Objects. | These samples assume basic familiarity with the use of the platform's Java APIs. In many cases, it is also helpful to be conversant with the platform's System Objects. | ||
<br>''Learn more:'' | <br>''Learn more:'' | ||
Line 9: | Line 9: | ||
:* [[Java API]] (reference) | :* [[Java API]] (reference) | ||
==Class Template== | ===Class Template=== | ||
{{:Java Class Template}} | |||
{ | |||
{{ | ===Error Handling in Java Code=== | ||
{{:Java Error Handling}} | |||
===Add a Task to a Record=== | |||
==Add a Task to a Record== | |||
This example uses the <tt>addRecord</tt> API to create a new Task associated for the current record. | This example uses the <tt>addRecord</tt> API to create a new Task associated for the current record. | ||
Line 129: | Line 54: | ||
{{Note|Since a Task can be attached to a record in any object, <tt>related_to</tt> is a [[Multi Object Lookup]] field. It's data value therefore contains both an object identifier and the record identifier, separated by a colon.<br>''Learn more:'' [[Java API:Field Type Reference|Field Type Reference]] }} | {{Note|Since a Task can be attached to a record in any object, <tt>related_to</tt> is a [[Multi Object Lookup]] field. It's data value therefore contains both an object identifier and the record identifier, separated by a colon.<br>''Learn more:'' [[Java API:Field Type Reference|Field Type Reference]] }} | ||
==Batch Update of Related Record Owners== | ===Batch Update of Related Record Owners=== | ||
This example assumes that some collection of records is divided into "batches", and that each batch has a single owner. A Batch object contains header records, and each record in the collection has a lookup to the Batch object - which makes them ''related records''. | This example assumes that some collection of records is divided into "batches", and that each batch has a single owner. A Batch object contains header records, and each record in the collection has a lookup to the Batch object - which makes them ''related records''. | ||
Line 136: | Line 61: | ||
The solution is to create a Java method that does the updates, and invoke it only when the owner field changes in the Batch record. | The solution is to create a Java method that does the updates, and invoke it only when the owner field changes in the Batch record. | ||
===Defining the Class=== | ====Defining the Class==== | ||
Here's a template for the class. It assumes that: | Here's a template for the class. It assumes that: | ||
:* The name of the batch object is "Batch_Controls" | :* The name of the batch object is "Batch_Controls" | ||
Line 228: | Line 153: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===Creating the Update Rule=== | ====Creating the Update Rule==== | ||
The next step is create an event Rule that runs only when the <tt>owner_id</tt> field changes in a Batch record: | The next step is create an event Rule that runs only when the <tt>owner_id</tt> field changes in a Batch record: | ||
Line 236: | Line 161: | ||
# Action: Invoke the Java method, <tt>updateRelatedRecords()</tt> | # Action: Invoke the Java method, <tt>updateRelatedRecords()</tt> | ||
===Learn more=== | ====Learn more==== | ||
:* [[Rules#Execution Criteria]] | :* [[Rules#Execution Criteria]] | ||
:* [[REST API:exec Resource]] | :* [[REST API:exec Resource]] | ||
==Manage Files== | ===Manage Files=== | ||
Files can be added to a specific field in a record, or added to the list of attachments. | Files can be added to a specific field in a record, or added to the list of attachments. | ||
===Add a File to a Record=== | ====Add a File to a Record==== | ||
If a record contains a field of type <tt>File</tt>, use this code to add file content to that field. | If a record contains a field of type <tt>File</tt>, use this code to add file content to that field. | ||
Line 277: | Line 202: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===Add and Access Record Attachments=== | ====Add and Access Record Attachments==== | ||
==== Generate an Attachment ==== | ===== Generate an Attachment ===== | ||
{{:Code:Generate an Attachment}} | {{:Code:Generate an Attachment}} | ||
====Add an External Attachment==== | =====Add an External Attachment===== | ||
Use this code to add an image, document, or file to a record as an attachment. As before, the byte array is presumed to have come from a [[Web Service]] or [[HttpConnection]]: | Use this code to add an image, document, or file to a record as an attachment. As before, the byte array is presumed to have come from a [[Web Service]] or [[HttpConnection]]: | ||
:<syntaxhighlight lang="java" enclose="div"> | :<syntaxhighlight lang="java" enclose="div"> | ||
Line 330: | Line 255: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
====Process Attachments==== | =====Process Attachments===== | ||
Use this method to process attachments for the current record: | Use this method to process attachments for the current record: | ||
:<syntaxhighlight lang="java" enclose="div"> | :<syntaxhighlight lang="java" enclose="div"> | ||
Line 387: | Line 312: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
== Set Case Status According to Related Tasks == | === Set Case Status According to Related Tasks === | ||
This code sample sets the Case state to Resolved or Pending, depending on the status of its related Tasks. Comments in the code explain its behavior. The expectation is that code is invoked by a Rule that is triggered when a Task's status changes. | This code sample sets the Case state to Resolved or Pending, depending on the status of its related Tasks. Comments in the code explain its behavior. The expectation is that code is invoked by a Rule that is triggered when a Task's status changes. | ||
Line 418: | Line 343: | ||
* 1. This code assumes that "Waiting" has been added to the list of "Case Task Status" values | * 1. This code assumes that "Waiting" has been added to the list of "Case Task Status" values | ||
* 2. Only processes that have started generate tasks, so all relevant processes should | * 2. Only processes that have started generate tasks, so all relevant processes should | ||
* launched manually or by rule before any tasks are completed. Otherwise, the case | * be launched manually or by rule before any tasks are completed. Otherwise, the case | ||
* status may be prematurely set to "Resolved". | * status may be prematurely set to "Resolved". | ||
*/ | */ | ||
Line 516: | Line 441: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Add and Access Notes== | ===Add and Access Notes=== | ||
When you add a Task or Attachment, an entry is added to the record history. But when you add a Private Note, you add it directly to the History object. There, the <tt>category</tt> field (value ="29") distinguishes Private Notes from other entries in the history, including email messages and other activities. | When you add a Task or Attachment, an entry is added to the record history. But when you add a Private Note, you add it directly to the History object. There, the <tt>category</tt> field (value ="29") distinguishes Private Notes from other entries in the history, including email messages and other activities. | ||
Line 523: | Line 448: | ||
''Learn more:'' [[System Objects#Adding and Accessing Related System Object Records|Adding and Accessing Related System Object Records]] | ''Learn more:'' [[System Objects#Adding and Accessing Related System Object Records|Adding and Accessing Related System Object Records]] | ||
===Add a Note=== | ====Add a Note==== | ||
This example adds a note with special handling instructions to a claim record. In this scenario, there is a Rule that is triggered by adding a record when certain conditions are met (for example, when the record pertains to a particular customer, or when a particular flag has been set). The Rule then invokes the method defined here. | This example adds a note with special handling instructions to a claim record. In this scenario, there is a Rule that is triggered by adding a record when certain conditions are met (for example, when the record pertains to a particular customer, or when a particular flag has been set). The Rule then invokes the method defined here. | ||
Line 573: | Line 498: | ||
:::'''Special handling instructions:'''<br>...notes.... | :::'''Special handling instructions:'''<br>...notes.... | ||
===Access Notes=== | ====Access Notes==== | ||
For Tasks and Attachments, you search the appropriate object looking for records that have a <tt>related_to</tt> value equal to the ID of the record you're interested in. To access notes, you search the History object, and you add one additional filter to restrict to the search to records who have the value "29" in the record's <tt>category</tt> field. That's the value that identifies the record as a Private Note, rather than an email or the recording of some action that was taken on the record. | For Tasks and Attachments, you search the appropriate object looking for records that have a <tt>related_to</tt> value equal to the ID of the record you're interested in. To access notes, you search the History object, and you add one additional filter to restrict to the search to records who have the value "29" in the record's <tt>category</tt> field. That's the value that identifies the record as a Private Note, rather than an email or the recording of some action that was taken on the record. (To see the list of possible values, go to '''[[File:GearIcon.png]] > Global Picklists > History Category'''.) | ||
:<syntaxhighlight lang="java" enclose="div"> | :<syntaxhighlight lang="java" enclose="div"> | ||
Line 615: | Line 540: | ||
Logger.info("Note: " + text, "Search"); | Logger.info("Note: " + text, "Search"); | ||
if (text.contains("...test for a specific note...")) { | if (text.contains("...test for a specific note...")) { | ||
// Operate on a specific | // Operate on a specific note here | ||
break; | break; | ||
} | } | ||
Line 623: | Line 548: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Send an Email Message== | ===Send an Email Message=== | ||
Start by getting an email address. Here, we'll get it from the record for a Contact with a known ID ("John Smith"): | Start by getting an email address. Here, we'll get it from the record for a Contact with a known ID ("John Smith"): | ||
Line 667: | Line 592: | ||
Like other Java API calls such as <tt>addRecord</tt> and <tt>searchRecords</tt>, <tt>sendEmailUsingTemplate</tt> returns a <tt>Result</tt> object whose methods you can call to check the result of the call. When <tt>sendEmailUsingTemplate</tt> succeeds, <tt>Result.getCode</tt> returns zero (0). | Like other Java API calls such as <tt>addRecord</tt> and <tt>searchRecords</tt>, <tt>sendEmailUsingTemplate</tt> returns a <tt>Result</tt> object whose methods you can call to check the result of the call. When <tt>sendEmailUsingTemplate</tt> succeeds, <tt>Result.getCode</tt> returns zero (0). | ||
== | ===Get the Name of an Object=== | ||
This code | This small routine is useful for general-purpose code that could be running on a variety of objects. Given the object's ID, it returns the singular version of the object name. It's used later, in the sample code that sends notification messages when a record has been updated. | ||
:<syntaxhighlight lang="java" enclose="div"> | :<syntaxhighlight lang="java" enclose="div"> | ||
< | /* | ||
* Get singular object name.<br> | |||
* (Does not work for User and Team objects.) | |||
* | |||
* @param objectID The object's identifier, as passed in the incoming Parameters | |||
* passed to a method called from the platform. | |||
*/ | |||
public String objectName(String objectID) throws Exception | |||
{ | |||
try { | |||
CustomObjectMetaDataBean mdb = Functions.getObjectMetaData(objectID); | |||
// For plural object name, use getDisplayTitle(). | |||
// (Note the mispelling of "singular" in the API.) | |||
return mdb.getSingluarDisplayTitle(); | |||
} | |||
catch (Exception e) { | |||
String msg = e.getMessage() + "\n methodName(): "+e.getClass().getName(); | |||
Functions.throwError(msg); | |||
return ""; // Required to keep the compiler happy | |||
} | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
= | ===Invoke Web Services Programmatically=== | ||
==Invoke Web Services Programmatically== | |||
These examples show: | These examples show: | ||
:* How to invoke a Web Service from Java Code | :* How to invoke a Web Service from Java Code | ||
Line 768: | Line 625: | ||
:* How to add a button to a form, and invoke that code (or any other) when the button is clicked. | :* How to add a button to a form, and invoke that code (or any other) when the button is clicked. | ||
===Invoke a Web Service from Java Code=== | ====Invoke a Web Service from Java Code==== | ||
This code sample invokes a Web Service directly from a Java class. The Web Service used in this example is one of the platform's REST APIs. In this case, it's the API to [[REST API:record Resource#Retrieve a Record|get a record]]. That API is called as though it is an external service, using the same techniques you will employ for any services you connect to. | This code sample invokes a Web Service directly from a Java class. The Web Service used in this example is one of the platform's REST APIs. In this case, it's the API to [[REST API:record Resource#Retrieve a Record|get a record]]. That API is called as though it is an external service, using the same techniques you will employ for any services you connect to. | ||
Line 878: | Line 735: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===Invoke a Web Service from a JSP Page=== | ====Invoke a Web Service from a JSP Page==== | ||
This JSP page displays a simple form that uses the Web Service Proxy defined above. When the user clicks '''[Search]''', retrieved data is displayed: | This JSP page displays a simple form that uses the Web Service Proxy defined above. When the user clicks '''[Search]''', retrieved data is displayed: | ||
: [[File:WebServiceJspFormExample.png]] | : [[File:WebServiceJspFormExample.png]] | ||
Line 991: | Line 848: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=== | ====Invoke a Web Service from a Form Button==== | ||
For information about invoking a web service from a form button, see the tech community article [https://tech.forums.softwareag.com/t/invoke-web-services-using-form-script/237544 Invoking a Web Service Using Form Script]. | |||
: | |||
/ | |||
form | |||
===Search and Update=== | |||
==Search and Update== | |||
To update a record, this example follows these steps: | To update a record, this example follows these steps: | ||
:*[[#Search for Account and Contact Records|Search for Account and Contact Records]] | :*[[#Search for Account and Contact Records|Search for Account and Contact Records]] | ||
Line 1,085: | Line 860: | ||
In this example, the Contact record that was created in the [[#Add a Contact Record|Add a Contact Record]]example is updated by associating it to the Account record created in the [[#Add an Account Record|Add an Account Record]] example. | In this example, the Contact record that was created in the [[#Add a Contact Record|Add a Contact Record]]example is updated by associating it to the Account record created in the [[#Add an Account Record|Add an Account Record]] example. | ||
===Search for Account and Contact Records=== | ====Search for Account and Contact Records==== | ||
Follow these general steps to search for records: | Follow these general steps to search for records: | ||
# Call <tt>searchRecords</tt> | # Call <tt>searchRecords</tt> | ||
Line 1,110: | Line 885: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
====Check the Result==== | ''Learn more:'' [[JAVA API:Filter Expressions in JAVA APIs]] | ||
=====Check the Result===== | |||
The result code for <tt>searchRecords</tt> works a little differently than for <tt>addRecord</tt>. | The result code for <tt>searchRecords</tt> works a little differently than for <tt>addRecord</tt>. | ||
Line 1,150: | Line 927: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
====Process the Returned Records==== | =====Process the Returned Records===== | ||
In earlier examples, the <tt>Parameters</tt> object was shown to hold name-value pairs for fields for when the <tt>addRecord</tt> call is made. | In earlier examples, the <tt>Parameters</tt> object was shown to hold name-value pairs for fields for when the <tt>addRecord</tt> call is made. | ||
Line 1,196: | Line 973: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===Relate the Account Record to the Contact Record=== | ====Relate the Account Record to the Contact Record==== | ||
As objects are manipulated in the platform (added, updated, deleted), associations between objects must be maintained. | As objects are manipulated in the platform (added, updated, deleted), associations between objects must be maintained. | ||
Line 1,285: | Line 1,062: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===Send Notification Messages to Followers=== | |||
{{:HowTo:Send Notification Messages to Followers}} | |||
<noinclude> | <noinclude> | ||
[[Category:Java API|1]] | [[Category:Java API|1]] | ||
</noinclude> | </noinclude> |
Latest revision as of 05:32, 17 May 2024
1 About the Code Samples
These samples assume basic familiarity with the use of the platform's Java APIs. In many cases, it is also helpful to be conversant with the platform's System Objects.
Learn more:
2 Class Template
Use this class as a template for a class that accesses record data and uses it to perform some operation. (The class is intentionally overly-complete. It's a lot easier to remove something you don't need than it is to look up the syntax for things you do need--or to know that syntax even exists.)
- <syntaxhighlight lang="java" enclose="div">
package com.platform.yourCompany.yourApplication;
// Basic imports import com.platform.api.*; import java.util.*;
// Reference static functions without having to specify the Functions class. // So Functions.throwError() can be written as throwError(). // (Code is shorter that way, but it's less obvious where things are defined.) import static com.platform.api.Functions.*;
// These are needed for advanced operations. //import com.platform.beans.*; //import static com.platform.api.CONSTANTS.*;
public class YourClass {
// Convenience methods to display a message to the user or add it the debug log. // Note: // When showMessage() is called multiple times, the strings are concatenated. // One long string is then displayed when the code returns to the platform. // We add an HTML linebreak (
) to separate them. Log messages, on the // other hand, use a Java "\n" (newline) character. public void show(String msg) throws Exception { Functions.showMessage(msg+"
"); } public void log(String msg) throws Exception { Logger.info(msg, "YourClass"); } public void debug(String msg) throws Exception { show(msg); log(msg); }
/** * CALLED FROM THE PLATFORM (hence the Parameters argument) */ public void doSomething(Parameters p) throws Exception { try { //Record incoming parameters in the log //log( "Method params:\n"+ p.toString().replace(",","\n") );
String objectID = p.get("object_id"); String recordID = p.get("id");
// Define the parameters for some operation Parameters params = Functions.getParametersInstance(); params.add("key", "value"); //...
// Do it. // Result.getCode() >= 0 on success, -1 on failure Result r = Functions.doSomething(params); if (r.getCode() < 0) { // Display message to user, add an entry to debug log, and // roll back the current transaction (no changes are committed). String msg = "Error <doing something>:\n"+ r.getMessage(); Functions.throwError(msg); } debug("Success"); } catch (Exception e) { String msg = e.getMessage() + "\n methodName(): "+e.getClass().getName(); log(msg); Functions.throwError(msg); } }
/** * CALLED INTERNALLY(a utility function of some sort) */ public String getSomeValue(String x) throws Exception { try { ... } catch (Exception e) { String msg = e.getMessage() + "\n methodName(): "+e.getClass().getName(); Functions.throwError(msg); } }
/** * UNIT TEST. (Note the @TestMethod pragma) */ @TestMethod public void test1_DescribeTheTestHere() throws Exception { String expect = "some result"; String actual = methodThatReturnsSomeResult(); RunTest.assertEquals(expect, actual); }
} // end class </syntaxhighlight>
Best Practice:
- Wrap code in a try..catch block, to guard against unexpected exceptions. (If not caught, they are simply ignored, and the method fails silently.)
- When you detect an error, put a detailed message into the Debug Log. Then call Functions.throwError to generate an exception, display a message for the user, and roll back the current transaction.
3 Error Handling in Java Code
The goal of error handling is identify the error that occurred, where it happened, and (ideally) what data was present at the time. The ideas presented in this section can help to achieve those goals.
3.1 Error Handling Tools
The Java Class Template embodies the error handling principles explained below. To do so, it uses the following tools:
- Logger.info - Put a text message into the Debug Log. Add "/n" (newline) to create a line break.
- None of the other tools put an entry into the log. This is the only one that does.
- Functions.showMessage - Display an HTML message onscreen. Add "<br>" to create a line break.
- Multiple calls to showMessage() are concatenated in the message buffer--but only if no Exceptions occurred.
- The contents of the message buffer are displayed when the code returns to the platform.
- Functions.throwError - Raise an exception to discontinue processing, display a message, and roll back the current transaction.
- The required message argument (Functions.throwError("some message") is displayed to the user.
- That call is equivalent to throw new Exception("some message").
- Those calls overwrite the message buffer, replacing any previous calls and any stored text from calls to showMessage. so only the last call is seen by the user.
- Whenever such a message is displayed, the Debug Log should contain detailed information on the cause. Follow the steps in the next section to be sure it does.
- Logger.info - Put a text message into the Debug Log. Add "/n" (newline) to create a line break.
- Learn more:
- To test various error conditions and see the results, use this class: ErrorHandlingTest Class.
Tip:
By all means, take advantage of the Unit Test Framework to identify and fix bugs before your users see them.
3.2 Error Handling Principles
- Errors are ignored unless you throw them. So:
- a. All calls to platform functions and standard Java functions should be in a try-catch block.
- b. Any code that sees an error (whether inside the try or catch) should call Functions.throwError.
- Nothing goes into the log unless you put there. Be sure to capture the information you need for debugging.
- A standard Java stack trace is of little value, since it is almost entirely the sequence of platform calls that got to your code. You're more interested in the steps your program followed. To get that information, catch every exception and add the name of the current method to the log, along with the exception's class name:
- a. Call Logger.info. Use the class name as the "category" label (2nd parameter).
- b. Include the method name in the message.
- c. Include the exception's class name, using e.getClass().getName().
- (For a standard Java exception like ArrayIndexOutOfBoundsException, the class name will generally tell you what went wrong.)
3.3 Error Handling Snippets
The following code fragments embody those principles:
- For normal code in a try-block (for example, when you get back an unexpected value from a call), generate an exception to interrupt processing and roll back the current transaction:
- <syntaxhighlight lang="java" enclose="div">
// Result.getCode() >= 0 on success, -1 on failure Result r = somePlatformAPI(); if (r.getCode() < 0) {
// THROW THE ERROR String msg = "Error <doing something>:\n"+ r.getMessage(); Functions.throwError(msg);
} </syntaxhighlight>
- For a top-level method that is called by the platform, generate a message, log it, and throw it:
- <syntaxhighlight lang="java" enclose="div">
try {
// TOP LEVEL CODE
} catch (Exception e) {
String msg = e.getMessage() + "\n yourMethod(): "+e.getClass().getName(); Logger.info( msg, "YourClass" ); // Additional log statement Functions.throwError(msg);
} </syntaxhighlight>
- For an internal method that is called by your code, generate the message and throw it:
- <syntaxhighlight lang="java" enclose="div">
try {
// INTERNAL CODE
} catch (Exception e) {
String msg = e.getMessage() + "\n yourMethod(): "+e.getClass().getName(); Functions.throwError(msg);
} </syntaxhighlight>
- (There is no need to log the message here, as it will be logged in the code that catches it.)
- That style of error handling generates log entries like these:
- someCallingMethod(): Exception
- someCalledMethod(): ArrayIndexOutOfBoundsException
3.4 Tracing Code Execution
- Calls to Functions.showMessage are useful in the normal flow of code, but not in a catch-block.
(You have to re-throw an exception to be sure it is seen. But when you re-throw it, the message it contains is the only thing the user sees.) - Use showMessage() calls to put breadcrumbs in a block of code. That works as long as no exceptions occur, and the messages are easy to see. But putting the messages in the Debug Log is more reliable. To get the best of both worlds, do both.
- The debug method in the Java Class Template is designed for tracing code execution. If the code runs properly, you see the trace messages onscreen. If not, they are still in the Debug Log.
- <syntaxhighlight lang="java" enclose="div">
public void debug(String msg) throws Exception { show(msg); log(msg); } </syntaxhighlight>
4 Add a Task to a Record
This example uses the addRecord API to create a new Task associated for the current record.
- <syntaxhighlight lang="java" enclose="div">
import com.platform.api.Functions; import com.platform.api.Parameters; import java.util.*;
public class RecordAdditions {
public void addTask(Parameters p) throws Exception { String objectID = p.get("object_id"); String recordID = p.get("id");
String subject = "New Task Record"; String description = "This task needs to be accomplished."; String relatedTo = objectID + ":" + recordID; //ex: "cases:99999999"
// Create the Task parameters Parameters taskParams = Functions.getParametersInstance(); taskParams.add("subject", subject); taskParams.add("description", description); taskParams.add("related_to", relatedTo); taskParams.add("due_date", new Date() ); // Add other fields like owner_id, as required // Add the Task Result r = Functions.addRecord("tasks", taskParams);
// Check the result here. // On success, Result.getCode() returns zero (0) // On failure, it returns -1 }
} </syntaxhighlight>
Note: Since a Task can be attached to a record in any object, related_to is a Multi Object Lookup field. It's data value therefore contains both an object identifier and the record identifier, separated by a colon.
Learn more: Field Type Reference
5 Batch Update of Related Record Owners
This example assumes that some collection of records is divided into "batches", and that each batch has a single owner. A Batch object contains header records, and each record in the collection has a lookup to the Batch object - which makes them related records.
The goal is to update all of the Related records when there is a change to the Batch record. In this case, the idea is to change the owner of all Related records whenever the Batch record is assigned to a new owner.
The solution is to create a Java method that does the updates, and invoke it only when the owner field changes in the Batch record.
5.1 Defining the Class
Here's a template for the class. It assumes that:
- The name of the batch object is "Batch_Controls"
- The name of the object with the related records is "Related_Details"
- The name of the Lookup field in that object is lookup_field
(Note that object and field names are somewhat different from the display labels that appear on screen. In particular, note that spaces are replaced by underscores.)
- <syntaxhighlight lang="java" enclose="div">
package com.platform.yourCompany.yourPackage;
import com.platform.api.*; import java.util.*;
public class BatchProcessing {
/** * Process records related to the current record. */ public void updateRelatedRecords(Parameters p) throws Exception { try { // Object names. (The internal names, not the display labels.) String batchObject = "Batch_Controls"; // Not used. Just for clarity. String relatedObject = "Related_Details";
// Get the owner of the current batch record //String objectID = p.get("object_id"); // Expected to be the Batch_Controls object String batchRecord = p.get("id"); String batchName = p.get("some_value_that_identifies_the_batch"); String batchOwner = p.get("owner_id");
// Search for Related records Result searchResult = Functions.searchRecords(relatedObject , "id", // Return record ID from search "lookup_field equals '" +batchRecord+ "'"); // lookup_field equals '9999999' int searchCode = searchResult.getCode(); if (searchCode < 0) { String msg = "Error searching for Related records"; log(msg + ":\n" + searchResult.getMessage()); Functions.throwError(msg); } else if (searchCode == 0) { log("No related records found for "+batchName); } else { // Records were found (searchCode equals the number) ParametersIterator pit = searchResult.getIterator(); while(pit.hasNext()) { // Get the record ID, for use when updating Parameters relatedParams = pit.next(); String relatedRecordID = relatedParams.get("id");
// Specify the field to update Parameters params = Functions.getParametersInstance(); params.add("owner_id", batchOwner);
// Update related record owner. (Functions.updateRecord() could // also be used, but this method has an option to send an email.) Result updateResult = Functions.changeOwnerShipInfo( relatedObject, relatedRecordID, batchOwner, false); //Set last param to true to send new owner a notification msg. int resultCode = updateResult.getCode(); if (resultCode < 0) { // Some error happened. String msg = "Update failed"; log(msg + ":\n" + updateResult.getMessage()); // Log the details Functions.throwError(msg + "."); // Abort and display error } } //while } //else } catch (Exception e) { // Catch surprises and report them. String msg = "Unexpected exception"; log(msg + ":\n" + e.getMessage() ); Functions.throwError(msg); } } // method public void log(String msg) throws Exception { // Put the message in the log. Logger.info(msg, "BatchProcessing"); } public void debug(String msg) throws Exception { // For interactive debugging. Displays in a popup // or at top of user's page, depending on context. Functions.showMessage(msg); log(msg); }
} // end class </syntaxhighlight>
5.2 Creating the Update Rule
The next step is create an event Rule that runs only when the owner_id field changes in a Batch record:
- > Objects > Batch > Business Rules
- Event Rules, On Batch (record): Owner Changed
- [New Rule]
- Action: Invoke the Java method, updateRelatedRecords()
5.3 Learn more
6 Manage Files
Files can be added to a specific field in a record, or added to the list of attachments.
6.1 Add a File to a Record
If a record contains a field of type File, use this code to add file content to that field.
For example, here we are adding an image for a book's cover. We assume that image was retrieved as a byte-array using a Web Service or an HttpConnection. The byte array is then passed to the method below, which converts it to an encoded string and stores it in the platform.
- <syntaxhighlight lang="java" enclose="div">
import com.platform.api.*; import com.platform.beans.*; import com.platform.api.utility.*; // Encoding class
public class RecordAdditions {
/** * Add a byte[] of document/image content to a record in the Books object */ public void addBookCover(byte[] bytes) throws Exception { Base64Codec base64 = new Base64Codec(); String encodedString = base64.encodeToString(bytes); PlatformFileBean file = new PlatformFileBean("cover image", encodedString);
// Add the file to a new record in the Books object, in the "bookCover" field Parameters params = Functions.getParametersInstance(); params.add("bookCover", file); Result result = Functions.addRecord("Books", params);
// Alternatively, update an existing record in the Books object // (Here the record ID is hardcoded. Generally, it would be passed as a parameter.) //String recordID = "76799333"; //Result result = Functions.updateRecord("Books", recordID, params); }
} </syntaxhighlight>
6.2 Add and Access Record Attachments
6.2.1 Generate an Attachment
This example uses a Document Template to generate a PDF or HTML document, and then attaches the document to the current case. It is expected that the method will be invoked from a Rule.
In outline, the process is:
- Get the record ID from the incoming method parameters.
- Use the generateDocument API to create a PDF (or HTML) document from an existing template.
- Use the getDocument API to retrieve it, in the form of a PlatformFileBean.
- Use the addRecord API to attach the document to the case.
- <syntaxhighlight lang="java" enclose="div">
package com.platform.yourCompany.yourPackage;
import com.platform.api.*; import com.platform.beans.*; //import java.util.*;
public class UtilityFunctions {
// This signature allows the method to be invoked from a rule. // We assume it is invoked on a Case record. public void generateAttachment(com.platform.api.Parameters inParams) throws Exception { String documentTitle = "PUT TITLE OF GENERATED DOCUMENT HERE"; String templateID = "PUT ID OF DOCUMENT TEMPLATE HERE";
// Get the record ID from the incoming parameters String objectID = inParams.get("object_id"); String recordID = inParams.get("id");
// Generate the document Result result = Functions.generateDocument(objectID, recordID, templateID, CONSTANTS.DOCUMENT.HTML); // or CONSTANTS.DOCUMENT.PDF int resultCode = result.getCode(); if (resultCode < 0) { String msg = "Document generation failed"; Logger.info(msg + ":\n" + result.getMessage(), "genAttachment"); Functions.throwError(msg + "."); return; }
// Retrieve the document as a PlatformFileBean String docID = result.getId(); result = Functions.getDocument(docID); resultCode = result.getCode(); if (resultCode < 0) { String msg = "Failed to retrieve the document"; Logger.info(msg + ":\n" + result.getMessage(), "genAttachment"); Functions.throwError(msg + "."); return; } Parameters docParams = result.getParameters(); PlatformFileBean fileBean = docParams.getPlatformFileBean(docID);
// Add the document as an attachment Parameters params = Functions.getParametersInstance(); params.add("title", documentTitle); params.add("file_field", fileBean ); params.add("related_to", objectID+":"+recordID); result = Functions.addRecord("attachments", params); resultCode = result.getCode(); if (resultCode < 0) { String msg = "Failed to attach document to case"; Logger.info(msg + ":\n" + result.getMessage(), "genAttachment"); Functions.throwError(msg + "."); return; } }
} </syntaxhighlight>
6.2.2 Add an External Attachment
Use this code to add an image, document, or file to a record as an attachment. As before, the byte array is presumed to have come from a Web Service or HttpConnection:
- <syntaxhighlight lang="java" enclose="div">
package com.platform.yourCompany.yourApplication;
import com.platform.api.*; import com.platform.beans.*; import com.platform.api.utility.*; // Encoding class
public class RecordAdditions {
/** * Add a byte[] of document/image content to the current record as an attachment. */ public void addAttachment(Parameters p, String fileName, byte[] bytes) throws Exception { // Get the ID of the current record String objectID = p.get("object_id"); String recordID = p.get("id"); String relatedTo = objectID + ":" + recordID; //ex: "cases:99999999"
Base64Codec base64 = new Base64Codec(); String encodedString = base64.encodeToString(bytes); PlatformFileBean file = new PlatformFileBean(fileName, encodedString);
// Add the file as an attachment to the current record Parameters params = Functions.getParametersInstance(); params.add("title", fileName); params.add("file_field", file); params.add("related_to", relatedTo); Result result = Functions.addRecord("attachments", params); }
/** * Test the addAttachment() method. Invoke this method from a Rule * and inspect the results in the GUI. (This method can be executed * from a Rule, because it has the required signature.) */ public void addAttachmentTest(Parameters p) throws Exception { String fileName = "testfile.txt"; String fileContent = "This is a test file."; byte[] bytes = fileContent.getBytes(); addAttachment(p, fileName, bytes); }
} </syntaxhighlight>
6.2.3 Process Attachments
Use this method to process attachments for the current record:
- <syntaxhighlight lang="java" enclose="div">
/** * Process the current record's attachments. To test this method, invoke * it from an update Rule and inspect the Debug Log. */ public void processAttachments(Parameters p) throws Exception { // Get the ID of the current record String objectID = p.get("object_id"); String recordID = p.get("id"); String relatedTo = objectID + ":" + recordID; //ex: "cases:99999999"
Result searchResult = Functions.searchRecords("attachments", "*", //all fields "related_to equals '" +relatedTo+ "'"); //related_to equals 'cases:9999999' int searchCode = searchResult.getCode(); if (searchCode < 0) { String msg = "Error searching Attachments"; Logger.info(msg + ":\n" + searchResult.getMessage(), "Search"); Functions.throwError(msg); } else if (searchCode == 0) { Logger.info("No attachments found for "+relatedTo, "Search"); } else { // Records were found (searchCode equals the number) Logger.info(searchCode+ " attachments found for " + relatedTo, "Search"); ParametersIterator pit = searchResult.getIterator(); while(pit.hasNext()) { // Log the search result. Optionally test for a specific record. Parameters attachParams = pit.next(); String title = attachParams.get("title"); Logger.info("Attachment title: "+title, "Search"); if (title.equals("...test for a specific attachment...")) { // Operate on a specific attachment here break; }
// Work with the search result. // Here, we assume it is text and put it into the log/ PlatformFileBean file = attachParams.getPlatformFileBean("file_field"); String encodedContent = file.getEncodedFileContent(); byte[] bytes = base64.decode(encodedContent); String text = new String(bytes); Logger.info("Attachment data: " + text, "Search"); // ... } } }
</syntaxhighlight>
7 Set Case Status According to Related Tasks
This code sample sets the Case state to Resolved or Pending, depending on the status of its related Tasks. Comments in the code explain its behavior. The expectation is that code is invoked by a Rule that is triggered when a Task's status changes.
- <syntaxhighlight lang="java" enclose="div">
package com.platform.acme.demo.TaskProcessing;
import com.platform.api.*; import static com.platform.api.Functions.*;
public class TaskProcessing {
public void log(String msg) throws Exception { // Put the message in the log. Logger.info(msg, "TaskProcessing"); } public void debug(String msg) throws Exception { // Display message in a popup or at top of page, depending on context, and log it. Functions.showMessage(msg); log(msg); }
/* * Call this code whenever a task changes status. * - In the Tasks object, create an Event Rule that runs when a record is updated * - Run the Rule when this expression is true: ISCHANGED(status) * Mark the case the task is attached to as "Resolved" if all tasks are completed. * Mark it "Pending" if all uncompleted tasks are pending (i.e. waiting on someone else). * NOTES: * 1. This code assumes that "Waiting" has been added to the list of "Case Task Status" values * 2. Only processes that have started generate tasks, so all relevant processes should * be launched manually or by rule before any tasks are completed. Otherwise, the case * status may be prematurely set to "Resolved". */ public void setCaseStatus(Parameters input_params) throws Exception { try { // Global Picklist "Case Status" defines the values for cases. String caseNew = "1"; String caseOpen = "2"; String casePending = "3"; String caseResolved = "4"; String caseClosed = "5"; String caseReopened = "6"; // Global Picklist "Case Task Status" defines the values for tasks. String taskOpen = "1"; String taskCompleted = "2"; String taskApproved = "3"; String taskRejected = "4"; String taskWaiting = "5";
// Default case status depends on the current task status. String taskID = input_params.get("id"); String taskStatus = input_params.get("status"); String caseFlag = ""; if (taskStatus == taskCompleted) { caseFlag = caseResolved; } else if (taskStatus == taskWaiting) { caseFlag = casePending; } else { // TODO: If case status is currently Resolved or Closed, it should change to Reopened. // (We ignore that scenario here. Fix it in production.) return; } // Format of the related_to field is object_id:record_id String relatedCase = input_params.get("related_to"); String caseObject = relatedCase.substring(0, relatedCase.indexOf(":")); String caseID = relatedCase.substring(relatedCase.indexOf(":")+1, relatedCase.length()); //debug("Case Obj: "+ caseObject +", ID: "+ caseID +", Task: "+ taskID);
// Get all task records related to the same case. Return the status field for each record. Result result = Functions.searchRecords("tasks","status,id","related_to = '"+relatedCase+"'"); int resultCode = result.getCode(); if (result.getCode() < 0) { // Display message to user and add an entry to debug log. String msg = "Task search failed. Code="+result.getCode(); log(msg); Functions.throwError(msg + "\n" + result.getMessage() ); } // Search returned a list of tasks. Examine each of them. ParametersIterator iterator = result.getIterator(); while (iterator.hasNext()) { Parameters params = iterator.next(); if (taskID == params.get("id")) continue; // The input record is included in the search results. Ignore it.
String status = params.get("status"); if ( status.equals(taskOpen) ) { // If there is an open task, case status does not change. caseFlag = ""; break; } else if ( status.equals(taskWaiting) ) { // If all remaining tasks are completed or waiting, // this setting will stand. caseFlag == casePending; } else { // Ignore completed/approved/rejected tasks. continue; } } if (caseFlag != "") { // All tasks are either waiting or completed. // Set the case status accordingly Parameters fcn_params = Functions.getParametersInstance(); fcn_params.add("status", caseFlag); Functions.updateRecord(caseObject, caseID, fcn_params); } } catch (Exception e) { // Catch surprises, display a popup, and put them in the log. String msg = "Unexpected exception"; log(msg + ":\n" + e.getMessage() ); Functions.throwError(msg + " - see debug log"); } }
} </syntaxhighlight>
8 Add and Access Notes
When you add a Task or Attachment, an entry is added to the record history. But when you add a Private Note, you add it directly to the History object. There, the category field (value ="29") distinguishes Private Notes from other entries in the history, including email messages and other activities.
To add a Private Note to the history, therefore, you must be sure to set that field value. Similarly, it must be part of the search criteria when retrieving history records, to eliminate all but the Private Note records in that object.
Learn more: Adding and Accessing Related System Object Records
8.1 Add a Note
This example adds a note with special handling instructions to a claim record. In this scenario, there is a Rule that is triggered by adding a record when certain conditions are met (for example, when the record pertains to a particular customer, or when a particular flag has been set). The Rule then invokes the method defined here.
To add the note, a record is added to the History object. The record is identified as a Private Note and set up so it references the appropriate Claim.
- <syntaxhighlight lang="java" enclose="div">
import com.platform.api.*; import java.util.*;
public class RecordAdditions {
public void addNote(Parameters p) throws Exception { // Get the ID of the current record String objectID = p.get("object_id"); String recordID = p.get("id");
// Formulate the Lookup-target for the note. // (A note can be added to a record in any object. So the Lookup target // is a combination of an object identifier and a record identifier.) String relatedTo = objectID + ":" + recordID; //ex: "cases:99999999"
// HTML tags can be included in the text String noteText = "Special handling instructions:
...notes...";
// Create the Note parameters Parameters noteParams = Functions.getParametersInstance(); noteParams.add("description", noteText); noteParams.add("related_to", relatedTo); noteParams.add("category", "29"); // Add the Note // result code is zero on success, -1 on failure Result r = Functions.addRecord("history", noteParams); if (r.getCode() < 0) { // Display message to user, add an entry to debug log, and // roll back the current transaction (no changes are committed). Functions.throwError("Failed:\n " + r.getMessage() ); } }
} </syntaxhighlight>
Notes:
- HTML tags included in the note text are interpreted when displayed. So this:
- <b>Special handling instructions:</b><br>...notes...
- displays like this:
- Special handling instructions:
...notes....
- Special handling instructions:
8.2 Access Notes
For Tasks and Attachments, you search the appropriate object looking for records that have a related_to value equal to the ID of the record you're interested in. To access notes, you search the History object, and you add one additional filter to restrict to the search to records who have the value "29" in the record's category field. That's the value that identifies the record as a Private Note, rather than an email or the recording of some action that was taken on the record. (To see the list of possible values, go to > Global Picklists > History Category.)
- <syntaxhighlight lang="java" enclose="div">
/** * Process the notes attached to the current record. To test this * method, invoke it from an update Rule and inspect the Debug Log. */ public void processNotes(Parameters p) throws Exception { // Get the ID of the current record String objectID = p.get("object_id"); String recordID = p.get("id"); String relatedTo = objectID + ":" + recordID; //ex: "cases:99999999"
Result searchResult = Functions.searchRecords("history", "*", //all fields "related_to equals '" +relatedTo+ "' AND category='29'"); //related_to equals 'cases:9999999' AND category = '29' int searchCode = searchResult.getCode(); if (searchCode < 0) { String msg = "Error searching History"; Logger.info(msg + ":\n" + searchResult.getMessage(), "Search"); Functions.throwError(msg); } else if (searchCode == 0) { Logger.info("No notes found for "+relatedTo, "Search"); } else { // Records were found (searchCode equals the number) Logger.info(searchCode+ " notes found for " + relatedTo, "Search"); ParametersIterator pit = searchResult.getIterator(); while(pit.hasNext()) { // Log the results. Do other processing as needed Parameters noteParams = pit.next(); String text = noteParams.get("description"); Logger.info("Note: " + text, "Search"); if (text.contains("...test for a specific note...")) { // Operate on a specific note here break; } } } }
</syntaxhighlight>
9 Send an Email Message
Start by getting an email address. Here, we'll get it from the record for a Contact with a known ID ("John Smith"):
- <syntaxhighlight lang="java" enclose="div">
Result getContactRecordResult = Functions.getRecord("CONTACT",
"first_name,last_name,email", contactRecID);
Logger.info("Sending Email to contact of Hello World Account..with email address:"
+(getContactRecordResult.getParameters()).get("email"), "SendMail");
</syntaxhighlight>
Then use the sendEmailUsingTemplate API to send a message to that person. In order, the parameters are:
- Related object identifier
- Identifier of the related record which is contactRecID that was retrieved previously
- To list which is set by making a nested call to Parameters.get to get the email address of the contact that was just retrieved
- Cc list which is set by making a nested call to Parameters.get to get the email address of the contact that was just retrieved
- Description (a text string)
- Identifier of an Email Template. The template is evaluated at run time, and values are substituted for template variables. The result becomes the body of the message.
- List of Document Templates identifiers to send as attachments (not used in this example).
- Note:
Do not choose a template based on a JSP page for use as an attachment.
Learn more: JSP Attachment Deprecation
- Note:
- List of document identifiers in your documents folder to send as attachments (not used in this example)
- <syntaxhighlight lang="java" enclose="div">
Result sendEmailResult = Functions.sendEmailUsingTemplate("CONTACT", contactRecID,
(getContactRecordResult.getParameters()).get("email"), (getContactRecordResult.getParameters()).get("email"), "Sending Email to Hello World's Primary Contact - John Smith", "1869974057twn1678149854", "", "");
Logger.info("Done sending mail from Hello World account's Contact John Smith",
"SendMail");
if(sendEmailResult.getCode() != 0) {
Logger.info("Error in sending email!" + sendEmailResult.getMessage(), "SendMail");
} else {
Logger.info("Success on sendEmail, check inbox : " + sendEmailResult.getMessage(), "SendMail");
} </syntaxhighlight>
Like other Java API calls such as addRecord and searchRecords, sendEmailUsingTemplate returns a Result object whose methods you can call to check the result of the call. When sendEmailUsingTemplate succeeds, Result.getCode returns zero (0).
10 Get the Name of an Object
This small routine is useful for general-purpose code that could be running on a variety of objects. Given the object's ID, it returns the singular version of the object name. It's used later, in the sample code that sends notification messages when a record has been updated.
- <syntaxhighlight lang="java" enclose="div">
/* * Get singular object name.
* (Does not work for User and Team objects.) * * @param objectID The object's identifier, as passed in the incoming Parameters * passed to a method called from the platform. */ public String objectName(String objectID) throws Exception { try { CustomObjectMetaDataBean mdb = Functions.getObjectMetaData(objectID); // For plural object name, use getDisplayTitle(). // (Note the mispelling of "singular" in the API.) return mdb.getSingluarDisplayTitle(); } catch (Exception e) { String msg = e.getMessage() + "\n methodName(): "+e.getClass().getName(); Functions.throwError(msg); return ""; // Required to keep the compiler happy } }
</syntaxhighlight>
11 Invoke Web Services Programmatically
These examples show:
- How to invoke a Web Service from Java Code
- How to call that code from a JSP page
- How to add a button to a form, and invoke that code (or any other) when the button is clicked.
11.1 Invoke a Web Service from Java Code
This code sample invokes a Web Service directly from a Java class. The Web Service used in this example is one of the platform's REST APIs. In this case, it's the API to get a record. That API is called as though it is an external service, using the same techniques you will employ for any services you connect to.
In this case:
- The method is invoked by a process, macro, or Rule that is attached to some record.
- We assume that the record contains an orderNum field that identifies an existing order.
- The Web Service is used to retrieve the order date and invoice number for that order.
- In the examples that follow, the orderNum is entered into a record form.
- If the orderNum is "1" or "2", we return test values, turning the class into a "mock stub" for the proxy. For "3", we assume that an error occurred, and return no data.
- We assume that the service address is http://orders.com/order, and that the number of the order to retrieve is passed as part of the URL: http://orders.com/order/{orderNum}. That strategy keeps the sample code simple.
- The structure of the returned data looks like this:
- <syntaxhighlight lang="xml" enclose="div">
<ORDER>
<ORDERNUM>...</ORDERNUM> <ORDERDATE>...</ORDERDATE> <INVOICENUMBER>...</INVOICENUMBER>
</ORDER> </syntaxhighlight>
- We assume that the Java orders package contains the order-processing classes.
- In that package, we create a class called WebServiceProxy, to manage connections to the external service.
- That class defines the getOrderDetails method, which uses the platform's HttpConnection Class to connect to the external service.
- The returned data structure is parsed using Java's XPathFactory.
- Invoking the Service
The method defined in this class invokes the web service:
- <syntaxhighlight lang="java" enclose="div">
package com.platform.yourCompany.orders;
import com.platform.api.*; import java.util.*; import java.io.StringReader; import javax.xml.xpath.*; import org.xml.sax.*;
import static com.platform.api.Functions.*;
public class WebServiceProxy {
public static final String WS_URL = "http://orders.com/order/"; /*---------------------------- Output from the external Web Service: <ORDER> <ORDERNUM>...</ORDERNUM> <ORDERDATE>...</ORDERDATE> <INVOICENUMBER>...</INVOICENUMBER> </ORDER> -----------------------------*/
//Define paths used for parsing public static final String ORDERDATE_ELEMENT = "ORDER/ORDERDATE"; public static final String INVOICENUMBER_ELEMENT = "/ORDER/INVOICENUMBER";
public Map getOrderDetails(Map params) throws Exception { Map<String,String> order = new HashMap<String,String>();
String orderNum = (String) params.get("orderNum"); if(orderNum != null && orderNum != "" ) { /** Return test values **/ if (orderNum.equals("1")) { order.put("orderDate", "11 Jan 2011"); order.put("invoiceNumber", "1111111111"); return order; } else if (orderNum.equals("2")) { order.put("orderDate", "22 Feb 2022"); order.put("invoiceNumber", "2222222222"); return order; } else if (orderNum.equals("3")) { // On error, return an empty data set return order; }
/** Execute the Web Service **/ String orderDate = ""; String invoiceNumber = ""; HttpConnection con = new HttpConnection(CONSTANTS.HTTP.METHOD.GET, WS_URL+orderNum); int code= con.execute(); String response = con.getResponse(); try { // Parse the response to retrieve order date and invoice#. XPathFactory factory = XPathFactory.newInstance(); XPath xPath = factory.newXPath(); orderDate = xPath.evaluate( ORDERDATE_ELEMENT, new InputSource(new StringReader(response)) ); invoiceNumber = xPath.evaluate( INVOICENUMBER_ELEMENT, new InputSource(new StringReader(response)) ); order.put("orderDate", orderDate); order.put("invoiceNumber", invoiceNumber); } catch(XPathExpressionException xpe) { Logger.info(xpe.getCause(), "HttpConnection"); showMessage("Parse of Web Service data failed. Check debug log."); } } return order; }
} </syntaxhighlight>
11.2 Invoke a Web Service from a JSP Page
This JSP page displays a simple form that uses the Web Service Proxy defined above. When the user clicks [Search], retrieved data is displayed:
Notes:
- JavaScript embedded in the page uses the platform's REST exec API to invoke the getOrderDetails() method.
- The Input XML sent to that method looks like this:
- <syntaxhighlight lang="xml" enclose="div">
<platform>
<execClass> <clazz>com.platform.yourCompany.orders.WebServiceProxy</clazz> <method>getOrderDetails</method> <orderNum>...</orderNum> </execClass>
</platform> </syntaxhighlight>
- The jQuery .ajax method is used to make the REST request and to process the returned data.
- The structure of the returned data is defined by the platform's REST exec API. (It's in JSON format, at the request of the AJAX call). The part of the structure we are concerned with here looks like this:
- <syntaxhighlight lang="xml" enclose="div">
{"platform": {
"execClass": { ... "orderNum": "..." "orderDate": "...", "invoiceNumber": "...", }, ...
}} </syntaxhighlight>
- JSP Page
- <syntaxhighlight lang="xml" enclose="div">
Order#: | <input type="text" id="orderNum" name="orderNum" size=10/> | <input type="button" id="submit" name="submit" value="Search"/> |
<script type="text/javascript">
$("#submit").click( function(){
var orderNum= $('#orderNum').val(); var inputXML = "<platform><execClass>" + "<clazz>com.platform.yourCompany.orders.WebServiceProxy</clazz>" + "<method>getOrderDetails</method>" + "<orderNum>" + orderNum + "</orderNum>" + "</execClass></platform>";
try { /* jQuery AJAX call to invoke REST API and process returned data */ $.ajax({ type: 'POST', url: '/networking/rest/class/operation/exec', contentType: 'application/xml', Accept:'application/json', data: inputXML, dataType: 'json', success: function(data) { /* Clear the results table. (jQuery method that removes */ /* element content, but not the element or its attributes.) */ $('#result').empty(); /* Size the table and make it visible */ $('#result').attr('border', '1'); $('#result').attr('style', 'width:300px'); var orderNum = data.platform.execClass.orderNum; if (orderNum) { var orderDate = data.platform.execClass.orderDate; var invoice = data.platform.execClass.invoiceNumber;
/* Add header rows and table data */ $('#result').append(
'Order# ' +'Order Date' +'Invoice Number'
); $('#result').append(
''+orderNum+'' +''+orderDate+'' +''+invoice+''
); } else { /* Call succeeded, but returned no data */
$('#result').append('No data found for that order.');
} } /* End of success function */
}); /*End of ajax call*/
} catch (e) { // JavaScript errors aren't recorded in the debug log. // Make sure we see them. alert(e); };
}); /* End of processing function */
</script> </syntaxhighlight>
11.3 Invoke a Web Service from a Form Button
For information about invoking a web service from a form button, see the tech community article Invoking a Web Service Using Form Script.
12 Search and Update
To update a record, this example follows these steps:
In this example, the Contact record that was created in the Add a Contact Recordexample is updated by associating it to the Account record created in the Add an Account Record example.
12.1 Search for Account and Contact Records
Follow these general steps to search for records:
- Call searchRecords
- Check the result
- Process the returned records
First, create a variable to hold the record identifier.
- <syntaxhighlight lang="java" enclose="div">
String contactRecID = null; </syntaxhighlight>
When a record is added database, the platform assigns it a unique record identifier. The value of a record identifier is opaque and is of no interest to the typical user. However, several Java API calls use the recordID parameter.
To get a record identifier, request the record_id field when making a searchRecords call. Find more information about arguments in searchRecords.
This example calls searchRecords for the CONTACT object. In order, the parameters to searchRecords in this example are:
- name of the object
- names of the fields to retrieve which includes record_id
- filter string (in this case, the filter string specifies that last_name contains "Smith" (the same as for the contact record previously created)
- <syntaxhighlight lang="java" enclose="div">
Result contactSearchResult = Functions.searchRecords("CONTACT", "record_id,first_name,last_name,email", "last_name contains 'Smith'"); </syntaxhighlight>
Learn more: JAVA API:Filter Expressions in JAVA APIs
12.1.1 Check the Result
The result code for searchRecords works a little differently than for addRecord.
Debug Example for searchRecords
This example checks the return code by calling Result.getCode, and takes some action, based on the return code:
- Return codes:
- less then zero (<0); the call is not successful
- equal to zero (=0); no records found
- greater than zero (> 0); successful and the value of the return code is the number of records returned
This code sample assigns the result code to a variable, which then defines the following action:
- If not successful, an error dialog is displayed
- If the code is zero, then a message is written to the debug log
- If successful, then the code is the number of records returned
- <syntaxhighlight lang="java" enclose="div">
int contactSearchCode = contactSearchResult.getCode();
if(contactSearchCode < 0) {
String msg = "Error searching CONTACT object"; Logger.info(msg + ":\n" + contactSearchResult.getMessage(), "Search"); Functions.throwError(msg + ".");
} else if(contactSearchCode == 0) {
Logger.info("Contact:No records found using the search function in CONTACT.", "Search");
} else {
// If the code "falls through" here, it means records were returned // that need to be processed; use the value of the return code in // the next section ...
} </syntaxhighlight>
12.1.2 Process the Returned Records
In earlier examples, the Parameters object was shown to hold name-value pairs for fields for when the addRecord call is made.
The searchRecords call uses the Parameters object, but in a different way - the searchRecords call returns the set of fields that were requested for each record that matches the search criteria as an array of Parameters objects.
To process each Parameters object in the search results, create an instance of ParametersIterator and then specify ParametersIterator.hasNext as the expression of a while loop.
The example resumes with the code "falling through" when checking the result code for searchRecords, meaning that the result code is greater than zero, which is the number of records returned.
The following code sample:
- Creates an instance of ParametersIterator by calling Result.getIterator
- Sets up a while loop with ParametersIterator.hasNext as the expression to evaluate at each iteration; Within the while loop, the code will:
- Creates an instance of Parameters by calling ParametersIterator.next
- Calls Parameters.get, specifying record_id as the parameter to get the value of the record identifier field which is assigned to a variable named contactRecordId
- Makes a Java API getRecord call with these parameters:
- Object identifier
- List of fields to get
- The record identifier which is contactRecordId in this case
- Makes a nested call to Result.getParameters to get the value of the first_name field; Checks if it is equal to "John". If so, the contactRecordId is assigned to the variable named contactRecID and the code breaks out of the loop; The contactRecID variable is used later when calling updateRecord, and sendEmailUsingTemplate
- <syntaxhighlight lang="java" enclose="div">
{
// "Falling through" here means records were returned Logger.info("Search for John Smith: Number of records found using search function in Contact:" + contactSearchCode, "Search"); ParametersIterator contactIterator = contactSearchResult.getIterator(); while(contactIterator.hasNext()) { Parameters contactParams = contactIterator.next(); String contactRecordId = contactParams.get("record_id"); Result getContactRecordResult = Functions.getRecord("CONTACT", first_name,last_name,flag_primary_contact,description", contactRecordId); String firstName = (getContactRecordResult.getParameters()).get("first_name"); Logger.info("Result from getRecord:\n" + getContactRecordResult.getMessage(), "Search"); Logger.info("Return code from getRecord:" + getContactRecordResult.getCode(), "Search"); Logger.info("First name retrieved : " + firstName, "Search"); if(firstName.equals("John")) { contactRecID = contactRecordId; break; } }
} </syntaxhighlight>
12.2 Relate the Account Record to the Contact Record
As objects are manipulated in the platform (added, updated, deleted), associations between objects must be maintained.
For example, when a Contact is created, it must be related to an Account:
- In the platform user interface, objects are related using the Lookup field as described in Relating Objects Using Lookups
- In the Java API, objects are related in code by searching for an object and then using a field value that identifies the object to set a field in a related object
Define the Relationship between Objects In this example, the code searches for an Account and then uses the Account Number to set a field in the contact. When relating a record in the Account object to a record in the Contact object, these three key-value pairs are required to define the relationship:
- reference_type
- related_to_id
- related_to_name
The following code searches for an account where the name field contains the text string "Hello". The Account record is created in Add an Account Record. The code follows the same model for a Update Records: call searchRecords, check the result code, and loop to process the returned records.
The following code sample:
- Sets up a while loop with ParametersIterator.hasNext as the expression to evaluate at each iteration. Within the while loop, the code creates two instances of Parameters:
- The first instance is created by calling getParametersInstance and is named updateContactParams; This instance is used to update the contact record later
- The second instance is created by calling ParametersIterator.next and is called accountParams
- Calls accountParams.get, specifying record_id as the parameter to get the value of the record identifier field, which is assigned to a variable named accountRecordId.
- Makes a Java API getRecord call using accountRecordId as a parameter
- Makes a nested call to Result.getParameters to get the value of the name field
- Checks if the name is equal to "Hello World"; If it is, the code adds some name-value pairs to updateContactParams
- Assigns the accountRecordId retrieved in the previous account search to related_to_id (This is how record identifiers are used to relate one object to another in the Java API)
- Makes an updateRecord call, specifying contactRecID and updateContactParams as parameters, which updated the Contact record.
- Checks the result in the final lines of code in the same way that previous examples did.
- <syntaxhighlight lang="java" enclose="div">
Result accountSearchResult = searchRecords ("ACCOUNT", "record_id,name,number,primary_contact_id",
"name contains 'Hello'");
int accountResultCode = accountSearchResult.getCode(); Logger.info(" Account:Result message for search ALL Account Records is "
+ accountSearchResult.getMessage(), "Search");
Logger.info(" Account:Result code for search ALL Record is " + accountResultCode,
"Search");
if(accountResultCode < 0) {
String msg = "Account could not be retrieved"; Logger.info(msg + ":\n" + accountSearchResult.getMessage(), "Search"); Functions.throwError(msg + ".");
} else if(accountResultCode == 0) {
Logger.info("Account:No records found using the search function in ACCOUNT.", "Search");
} else {
Logger.info( "Search for Hello World: Number of records found using search function in Account:" + accountResultCode, "Search"); ParametersIterator accountIterator = accountSearchResult.getIterator(); while(accountIterator.hasNext()) { Parameters updateContactParams = Functions.getParametersInstance(); Parameters accountParams = accountIterator.next(); String accountRecordId = accountParams.get("record_id"); Result getAccountRecordResult = Functions.getRecord("ACCOUNT", "name,description", accountRecordId); String accountName = (getAccountRecordResult.getParameters()).get("name"); Logger.info("Result from getRecord on ACCOUNT:\n" + getAccountRecordResult.getMessage(), "Search"); Logger.info("Return code from getRecord on ACCOUNT:" + getAccountRecordResult.getCode(), "Search"); if(accountName.equals("Hello World")) { Logger.info("Account record ID:" + accountRecordId, "Update"); updateContactParams.add("description", "Updating Contact"); updateContactParams.add("reference_type", "Account"); updateContactParams.add("related_to_id", accountRecordId); updateContactParams.add("related_to_name", accountName); Logger.info("Updating contact record with id:" + contactRecID, "Update"); Result contactUpdateResult = Functions.updateRecord("CONTACT", contactRecID, updateContactParams); if(contactUpdateResult.getCode() == 0) { Logger.info("Account:Contact of the Account record updated successfully\n" + contactUpdateResult.getMessage(), "Update"); } else { String msg = "Error updating contact"; Logger.info(msg + ":\n" + contactUpdateResult.getMessage(), "Update"); Functions.throwError(msg + "."); } } }
} </syntaxhighlight>
13 Send Notification Messages to Followers
This code sample sends an email to everyone who registered themselves as a "follower" of a Case (or any other object where the application designer allows it).
To follow the record, a "related record" with the user's email address is added to the Followers object. When the Case (or other record) is updated, an email notification is sent to everyone who is following it, using an API and an email template created for the purpose.
- Learn more:
- Related Records
- Email Templates
- SendEmailUsingTemplate (Java API)
13.1 Setup and Testing
- Create the Followers object:
- Launch the Object Construction Wizard
- Define the fields: user_name, email.
- Click [Save] and then [Create].
- Create the linking field:
- Go to > Objects > Followers > Fields
- Create a new Multi Object Lookup field called related_to.
- Select the objects that can be followed, or choose All Objects.
- (The data in those fields has the format object_id:record_id. That's why you can follow any record in the system.)
- Click [Save].
- Modify the Case form to create a new tab that displays Followers.
- Go to > Objects > Cases > Forms > Agent Form
- Click [New Related Information]:
- Object: Followers
- Fields to Link: Followers Field: Related To, links to Cases Field: ID
- Fields to Display: User Name, Email
(App ID will stay hidden, as it is basically unreadable.) - Sort by: Email, Order: Ascending
- Click [Save].
- Use the sample code below to create the Notifications class.
- Create a record-updated rule that invokes the notifyFollowers() method defined in the code.
- Name: Send Update Notifications
- Description: Tell anyone who is following the record that a change has occurred.
- Run this Rule: Unconditionally
- Actions to Perform: Invoke Method, Class: Notifications, Method: notifyFollowers
- Click [Save].
- Open a case record, click the Followers tab, and manually add yourself as a follower.
- Fill in the user and email fields.
- If you're in the ServiceDesk app, the app ID is easy. It's 1 (one).
- Otherwise, leave the field empty and delete the record later. Or go to the Applications page to retrieve the ID of the current app.
- Update the case record and check your inbox for the notification message.
13.2 Next Steps
Here are some things you can do after you get the basic behavior working:
- Define a Follow macro for any object in which you want to add Followers:
- In that macro, invoke the addFollower() method defined in the sample code.
- The macro will appear in the list of actions available for the record.
- When clicked, the macro will add a Follower record, with all fields filled in.
- Here's what you need for the basic macro:
- Name: Follow
- Description: Add the user who clicks the button to the list of "followers"--people who will receive an email when the record is updated.
- Show: Always
- Action: Invoke Method, Class: Notifications, Method: addFollower
- Send notifications for a subset of updates
- This is an extra credit assignment.
- To do it, you hard-code a list of fields you care about in the class. Or you could use the Functions.getFieldMetadata API to see send notifications only for Audited Fields.
- Allow for "unfollowing" (maybe)
- Users can unfollow a record manually by clicking their Follower record and choosing the Delete action. Alternatively, you could cannibalize the code below to add a method that searches for a Follower with a matching email_address and related_to field. (But then you have to remove the macro and add a JavaScript button to the form, instead. That JavaScript will then use the REST APIs to query the Followers object and display either a [Follow] or an [Unfollow] button--which makes a nice user interface, except that it adds lag time before the form appears.)
13.3 Email Template
- Template Name: Update Notification
- Type: Case Templates (rather than an SLA template)
- From Name: Support System
- From Email Address: $custom.support_team_email_address
- Subject: Case Record Updated
- Template Variables:
- Case Record Variables:
- $cases.case_number, $cases.modified_id.full_name, $cases.description
- Current Note Variable (case history): $__current_note__.description
- Custom Variable: $custom.support_team_email_address
- Case Record Variables:
Here is a sample Update Notification template that uses those variables:
- Case Record# $cases.case_number
- was updated by $cases.modified_id.full_name
- $__current_note__.description
- Case Description:
- $cases.description
The template HTML looks like this:
- <syntaxhighlight lang="java" enclose="div">
Case Record# <a href="https://{domain}/networking/servicedesk/index.jsp#_cases/$cases.case_number">$cases.case_number</a>
was updated by $cases.modified_id.full_name
$__current_note__.description
Case Description:
$cases.description
</syntaxhighlight>
13.4 Code
Tip:
Whether or not a field is included in the change-list depends on several aspects of the field--it's name, it's type, and (for extra credit) whether or not it is an Audited Field. That data is defined by the field's "metadata". There are several ways to get that information:- To see the kinds of field metadata that is defined https://{yourDomain}/networking/rest/field/{objectName}
- To see the metadata for a particular field, use https://{yourDomain}/networking/rest/field/{objectName}/{fieldName}
- To see samples of that data, see REST API:field Resource#Payload Examples
- <syntaxhighlight lang="java" enclose="div">
package com.platform.acme.demo;
// Basic imports import com.platform.api.*; import com.platform.beans.*; import com.platform.beans.EnumerationDetailsBean.*;
import java.util.*;
// Reference static functions without having to specify the Functions class. //import static com.platform.api.Functions.*; //import static com.platform.api.CONSTANTS.*;
/**
* Prerequisites: * 1. There is a Main object with records (e.g. cases) that people want to * "follow", so they receive an email notification when there is an update. * 2. There is an object that has related records, one for each person who is * following records in the Main object. This code assumes that the related * object is called "Followers". * 3. A related record is added to that object when someone becomes a follower. * 4. The related object has a Lookup field that points to the thing being followed, * an email address to send the notification to, and the ID of the application * the user was in when the follower was added. (The ID is used to create * a link to the record in the notification message.) * 5. The object being followed has a "description" field to include in the * message. (But you can easily modify the code to leave that out.) * * Record Management: * a. For initial testing, some "related records" can be created by hand. * b. In a production setting, a "Follow" action will add a related record. * c. To "unfollow", delete the related record in the UI. * * Usage: * 1. An update Rule on the main object should invoke the notifyFollowers() * method defined in this class. That method finds all related records and * sends the notification messages. * 2. The addFollower() method, meanwhile can be invoked by a macro defined * on any object which has records that users want to follow. * * Caveat: * This mechanism does catch the situation when a private note is added to * a record. (A rule could be added to the History object, but such rules * do not fire.) */
public class Notifications {
public void show(String msg) throws Exception { Functions.showMessage(msg); } public void log(String msg) throws Exception { Logger.info(msg, "Notifications"); } public void debug(String msg) throws Exception { show(msg); log(msg); }
/* * Get singular object name.
* (Does not work for User and Team objects.) * * @param objectID The object's identifier, as passed in the incoming Parameters * passed to a method called from the platform. */ public String objectName(String objectID) throws Exception { try { CustomObjectMetaDataBean mdb = Functions.getObjectMetaData(objectID); return mdb.getSingluarDisplayTitle(); } catch (Exception e) { String msg = e.getMessage() + "\n objectName(): "+e.getClass().getName(); Functions.throwError(msg); return ""; // Required to keep the compiler happy } }
/** * Let people know when a record they are following has been updated. *
Usage:
* Call this message from an update rule. */ public void notifyFollowers(Parameters p) throws Exception { //log( "Record params from rule:\n"+ m.toString().replace(",","\n") );
// Get information from the current record String objectID = p.get("object_id"); String recordID = p.get("id"); String userName = p.get("user"); // The user's full name String serverName = p.get("serverName");
String recordName = p.get("name"); // The record's human-readable identifier String recordDescr = p.get("description"); // We assume this field exists Parameters priorValues = p.getPriorParams(); // Records that point to this one have this in their "related_to" field. String target_record = objectID +":"+ recordID;
String objName = ""; try { objName = objectName(objectID); } catch (Exception e) { String msg = e.getMessage() + "\n notifyFollowers(): "+e.getClass().getName(); Logger.info( msg, "Notifications" ); // Additional log statement Functions.throwError(msg); }
// Get the activity details. (Report new values only.)
String modifiedFields = "
- \n";
try {
Map m = Functions.getFieldMetaDataMap(objectID);
Iterator fieldIterator = m.entrySet().iterator();
while ( fieldIterator.hasNext() ) {
// Compare fields to priorValues. Report those that differ.
// Skip the modified_date field.
Map.Entry entry = (Map.Entry) fieldIterator.next();
String field = (String) entry.getKey(); // Field name
if (field.equals("modified_date")) continue;
String newValue = p.get(field) == null ? "" : p.get(field);
String oldValue = priorValues.get(field);
oldValue = oldValue == null ? "" : oldValue;
if (newValue.equals(oldValue)) continue;
// Replace the record ID in a Lookup field w/a human-readable label.
FieldMetaDataBean fmdb = (FieldMetaDataBean) entry.getValue();
String type = fmdb.getType();
// **********************************************************
// EXTRA CREDIT: Ignore the field if it is not audited, so the
// email includes values displayed in the record history only.
// (FieldMetaDataBean.getIsTracked() returns true)
// ***********************************************************
if ( "LOOKUP".equals(type) && newValue != "" )
{
IDBean objectBean = fmdb.getLookUpObjectId();
String targetObject = objectBean.getValue();
// Get the list of fields that define the label for a record
RecordLocatorBean rlb = Functions.getRecordLocator(targetObject);
List<String> fieldList = rlb.getKeyColumns();
String fields = "";
for (String item : fieldList) {
if ( ! "".equals(fields) ) fields += ",";
fields += item;
}
Result r = Functions.getRecord(targetObject, fields, newValue);
Parameters recParams = r.getParameters();
// log( recParams.toString().replace(", ", "\n") );
String record_name = "";
for (String item : fieldList) {
if ( ! "".equals(record_name) ) record_name += " - ";
record_name += recParams.get(item);
}
newValue = record_name; // as defined by the Record Locator
if ( "".equals(newValue) ) newValue = "(unnamed record)";
}
else if ("MULTI_OBJECT_LOOKUP".equals(type))
{
// A Multi Object Lookup field has the form {objectID}:{recordID}
String[] parts = newValue.split(":");
String objectName = parts[0];
String targetRecord = parts[1];
// **********************************************************
// EXTRA CREDIT: Factor out the logic above that gets
// a record's identifying label, and call it here
// ***********************************************************
}
else if ("PICK_LIST".equals(type)
|| "GLOBAL_PICK_LIST".equals(type)
|| "DEPENDENT_PICK_LIST".equals(type)
|| "RADIO_BUTTON".equals(type))
{
// Replace enumeration value with display label
EnumerationDetailsBean edb = fmdb.getEnumerationDetails();
List<EnumerationItemBean> entries = edb.getEnumerationItems();
for (EnumerationItemBean item:entries) {
if (newValue.equals(item.getPicklistValue()) ) {
newValue = item.getPicklistLabel();
break;
}
}
}
else if ("MULTI_CHECK_BOX".equals(type))
{
// Process list of values. Replace with labels.
// **********************************************************
// EXTRA CREDIT: The field value is a comma-separated list
// of enumeration values. Split on comma to get the items,
// use the logic above to get their labels, and create a
// comma separated list of labels.
// ***********************************************************
}
else if ("CHECK_BOX".equals(type))
{
// Replace boolean 1 or 0 with text label
newValue = "No";
if ("1".equals(newValue)) newValue = "Yes";
}
else if ("FILE_FIELD".equals(type)
|| "IMAGE_FIELD".equals(type))
{
// Replace record identifier with file name
// Format: "{filename}:{documentID}"
String[] parts = newValue.split(":");
newValue = parts[0];
// **********************************************************
// EXTRA CREDIT: Make the file name a link.
// ***********************************************************
}
if (newValue == "") newValue = "(removed)";
modifiedFields += "
- "+ field +": "+ newValue +" \n"; } modifiedFields += "
\n";
} catch (Exception e) { String msg = e.getMessage() + "\n notifyFollowers(): " + e.getClass().getName(); Logger.info( msg, "Notifications" ); // Additional log statement Functions.throwError(msg); }
try { // Construct the message. This message assumes that the record // that was modified has a description field. String subject = objName + " record updated"; String ccList = ""; String recordURL = "https://"+ serverName + "/networking/servicedesk/index.jsp";
// Build a link to the record for use in the email // The link format is based on the object and the current application. Map<String,Object> m = Functions.getLoggedInUserInfo(); //log( "Logged-in User Info:\n"+ m.toString().replace(",","\n") ); String appID = (String)m.get("startingAppId"); String recordLink = ""; if (objName.equals("Case")) { // A Case record: // https://{domain}/networking/servicedesk/index.jsp#_cases/{caseNum} recordURL += "#_cases/"+ p.get("case_number"); } else if (objName.equals("Task") && appID == "1") { // A Task in the ServiceDesk app: // https://{domain}/networking/servicedesk/index.jsp#_tasks/{recID} recordURL += "#_tasks/"+ recordID; } else { // A record in another object, including tasks in other applications: // https://{domain}/networking/servicedesk/index.jsp?applicationId={appID}#_{objectID}/{recID} recordURL += "?applicationId="+ appID +"#_"+ objectID +"/"+ recordID; } // Formulate an HTML link to the record if (recordURL != "" && recordName != "") { recordLink = "<a href=\""+ recordURL +"\">"+ recordName +"</a>"; }
// Set up the email message
String body = "
"+ objName + " record " + recordLink +":
\n"
+ "was updated by "+ userName +"
\n"
+ modifiedFields
+ "
"+ objName +" description:
\n" + "
\n"
+ recordDescr
+ "\n
";
// Find all related Follower records that target the current record // Format of the criteria string is: related_to = '{object}:{record}' String relatedObject = "Followers"; Result result = Functions.searchRecords(relatedObject, "email", "related_to = '" + target_record +"'"); int resultCode = result.getCode(); if (resultCode > 0) { // Process the records that were found. // (If getCode() has a positive number, it was the number found.) int count = 0; ParametersIterator iterator = result.getIterator(); while(iterator.hasNext()) { Parameters params = iterator.next(); String toAddress = params.get("email");
// Send message (nulls are parameters for attachments) Result r = Functions.sendEmail( objectID, recordID, toAddress, ccList, subject, body, null, null); if (r.getCode() < 0) { String msg = "Error sending email:\n" + r.getMessage(); Functions.throwError(msg); } count++; } debug(count + " update notifications sent"); } } catch (Exception e) { // Catch surprises, display a popup, and put them in the log. String msg = e.getMessage() + "\n notifyFollowers(): "+e.getClass().getName(); Logger.info(msg, "Notifications"); // Additional log statement Functions.throwError(msg); } }
/* * Usage: This method should be called by Macro (i.e. a record Action) * on the record that the person wants to follow. (The form that displays * the record should also have a Related Information section, so the user * can remove their record. */ public void addFollower(Parameters p) throws Exception { try { //log( "Macro params:\n"+ p.toString().replace(",","\n") ); String objectID = p.get("object_id"); String recordID = p.get("id"); String lookup_target_record = objectID +":"+ recordID; // ex: cases:99999
Map<String,Object> m = Functions.getLoggedInUserInfo(); //log( "Logged-in User Info:\n"+ m.toString().replace(",","\n") ); String userName = (String)m.get("full_name"); String email = (String)m.get("email");
//************************************************* // EXTRA CREDIT: Exit here if a record already exists with those values //*************************************************
Parameters params = Functions.getParametersInstance(); params.add("object_id", "Followers"); // required param params.add("user_name", userName); params.add("email", email); params.add("related_to", lookup_target_record); params.add(PLATFORM.PARAMS.RECORD.DO_NOT_EXEC_RULES,"1"); // No rules Functions.addRecord("Followers", params); } catch (Exception e) { String msg = e.getMessage() + "\n addFollower(2): "+e.getClass().getName(); Logger.info( msg, "Notifications" ); // Additional log statement Functions.throwError(msg); } }
} // end class </syntaxhighlight>
Note:
The code above builds the HTML link to the record based on the object and the current application. The logic works correctly, 99.999% of the time. (Applications rarely share objects, even fewer share records in those objects, and it is only links to Task records that could differ.)It is theoretically possible (although highly unlikely) for the same Task record to be shared between two applications. In that scenario, it is also possible for a notification email generated in one application to be sent to someone who doesn't have access to that application. In that highly improbable scenario, the logic could fail.
To handle that situation, that appID could be stored in the Follower object. The code that generates the record-link for the message could then be moved inside of the loop that finds Follower records and sends emails. But doing that makes the logic more complicated and harder to comprehend, while at the same time reducing performance. So this code uses the "near-bulletproof" strategy.