Java Code Samples
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.)
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 (<br>) 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+"<br>"); } 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
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 class template above embodies the error handling principles explained here. To do so, they employ the following tools:
- Logger.info - Put a text message into the Debug Log. Use "/n" (newline) to create a line break.
- Functions.showMessage - Display an HTML message onscreen. Use <br> to create a line break.
- Note: Only one message is displayed, when the code returns to the platform. Multiple calls are concatenated.
- Functions.throwError - Raise an exception to discontinue processing and roll back the current transaction.
- Error-Handling Principles
- Use class name as "category" identifier when putting things into the log
- Include the method name in error messages
- When catching an unexpected exception, display the exception's class name.
That's generally more indicative than the message embedded in the exception - You can use Functions.throwErrror() to get a stack trace, but it generally doesn't help very much, because the trace is almost entirely devoted to the sequence of platform calls that got to your code. (You're more interested in the steps your program followed. Following these steps gives you that information.)
- All calls to platform functions need to be in a try…catch block.
- All calls to methods that invoke a platform function need to be in a try…catch block
- However, the operations in the catch-block are different, in those three cases:
- a. In the catch block for a method that is called from the platform:
// LOG, THROW, and SHOW String msg = "Unexpected exception in methodName()"; log(msg + ":\n" + e.getClass().getName() ); //Sometimes helpful: + "\n" + e.getMessage() ); show(msg + " - see debug log");
- b. In the catch block inside a method that is called by your code
// LOG and THROW String msg = "Unexpected exception in methodName()"; log(msg + ":\n" + e.getClass().getName() ); //+ "\n" + e.getMessage() ); throw e; --outside of a catch block, call Functions.throwError("msg");
- c. In the catch block surrounding the call you make to that method:
// SHOW show("Error in getActivities() - see debug log"); return;
4 Add a Task to a Record
This example uses the addRecord API to create a new Task associated for the current record.
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 } }
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.)
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
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.
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); } }
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.
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; } } }
Tip:
For a more general solution, add function parameters to specify the document title and the template to use. Then write a wrapper method to supply a specific template, something like this:public void generate_A1_Attachment(com.platform.api.Parameters inParams) throws Exception { String docTitle = "A1 Attachment"; String templateID = "9ewr8aasd923234ased0897234d"; generateAttachment(inParams, docTitle, templateID); }
Then, when you create the invocation Rule, you'll choose which wrapper method to use.
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:
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); } }
6.2.3 Process Attachments
Use this method to process attachments for the current record:
/** * 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"); // ... } } }
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.
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"); } } }
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.
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 = "<b>Special handling instructions:</b><br>...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() ); } } }
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.)
/** * 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; } } } }
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"):
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");
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)
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"); }
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 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)
10.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.
10.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.)
10.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:
<p><b>Case Record#</b> <a href="https://{domain}/networking/servicedesk/index.jsp#_cases/$cases.case_number">$cases.case_number</a> <br /> was updated by $cases.modified_id.full_name</p> <blockquote> $__current_note__.description </blockquote> <p><b>Case Description:</b></p> <blockquote> $cases.description </blockquote>
10.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
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.<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); 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. * <br>Usage:<br> * 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 = "<ul>\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 += "<li><b>"+ field +"</b>: "+ newValue +"</li>\n"; } modifiedFields += "</ul>\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 = "<p><b>"+ objName + " record " + recordLink +": </b><br>\n" + "was updated by "+ userName +"</p>\n" + modifiedFields + "<p><b>"+ objName +" description:</b></p>\n" + "<blockquote>\n" + recordDescr + "\n</blockquote>"; // 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
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.
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:
<ORDER> <ORDERNUM>...</ORDERNUM> <ORDERDATE>...</ORDERDATE> <INVOICENUMBER>...</INVOICENUMBER> </ORDER>
- 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:
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; } }
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:
<platform> <execClass> <clazz>com.platform.yourCompany.orders.WebServiceProxy</clazz> <method>getOrderDetails</method> <orderNum>...</orderNum> </execClass> </platform>
- 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:
{"platform": { "execClass": { ... "orderNum": "..." "orderDate": "...", "invoiceNumber": "...", }, ... }}
- JSP Page
<!--User enters record-selection data and clicks [Search].--> <table> <tr> <td> Order#: </td> <td><input type="text" id="orderNum" name="orderNum" size=10/></td> <td><input type="button" id="submit" name="submit" value="Search"/></td> </tr> </table> <br></br><br></br> <!--Returned data appears here. (With no borders, table starts out invisible.) --> <table id="result"> </table> <!--This script attaches an anonymous function to the form button. The function invokes the method in the WebServiceProxy class. --> <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( '<tr><td>Order# </td>' +'<td>Order Date</td>' +'<td>Invoice Number</td></tr>' ); $('#result').append( '<tr><td>'+orderNum+'</td>' +'<td>'+orderDate+'</td>' +'<td>'+invoice+'</td></tr>' ); } else { /* Call succeeded, but returned no data */ $('#result').append('<tr><td>No data found for that order.</td></tr>'); } } /* 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>
11.3 Add a Button to a Form
The JavaScript shown here adds a button to a standard object Form:
The standard form is shown on the left. ON LOAD JavaScript added to it injects the button, producing the form on the right. That code also defines the button's operation, so that clicking it calls the getOrderDetails method defined earlier.
Notes:
- The current form is referenced as _sdform.
- As in the previous example, an AJAX call is used to invoke the method in the Web Service Proxy.
- Form fields are then populated with the returned values.
- Learn more: Form Scripts
ON LOAD JavaScript:
// Get a pointer to the current form var form = _sdForm try { /* * Find the order number field, modify it's CSS properties, * and add a search button to the element it's contained in. * (Each label and field is contained in it's own <div>. The * .parent() function accesses it, and the button is added at the end.) */ form.find("#orderNum").css( {'width': '50%',"margin-bottom":"0px"}); form.find("#orderNum").removeClass("text_box_case"); form.find("#orderNum").parent().append( "<input type='button' id='lookup_order' name='lookup_order' value='Lookup Order'" + "style='background: none repeat scroll 0 0 #EBEBEB;box-shadow: 0 0 0 0 #888888 inset;" + " height: 26px;margin-top:-0px; padding: 0 8px 0 6px;" + " margin-right:5px;margin-left:5px;' >" ) ; /* * Add an anonymous function to the button to invoke the WebServiceProxy method. */ form.find("#orderNum").parent().find("#lookup_order").click( function() { // Create the structure to send to the proxy method var orderNum= getTextFieldValue(_sdForm, "orderNum"); var inputXML = "<platform><execClass>" + "<clazz>com.platform.yourCompany.orders.WebServiceProxy</clazz>" + "<method>getOrderDetails</method>" + "<orderNum>" + orderNum + "</orderNum>" + "</execClass></platform>"; // Use jQuery's ajax method to invoke the REST API $.ajax({ type: 'POST', url: '/networking/rest/class/operation/exec', contentType: 'application/xml', Accept:'application/json', data: inputXML, dataType: 'json', success: function (data) { // Call succeeded. if (data.platform.execClass.invoiceNumber) { // Copy data to form fields. setTextFieldValue(_sdForm, "invoice_number", data.platform.execClass.invoiceNumber); setTextFieldValue(_sdForm, "order_date_1462329883", data.platform.execClass.orderDate); } else { alert("No order found."); } } // End of success function }); // End of AJAX call // We're done. Don't submit the input form to the web service. // (We already did that, and it doesn't know what to do with form data.) return false; }); // End of button function } catch (e) { // JavaScript errors aren't recorded in the debug log. // Make sure we see them. alert(e); };
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.
String contactRecID = null;
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)
Result contactSearchResult = Functions.searchRecords("CONTACT", "record_id,first_name,last_name,email", "last_name contains 'Smith'");
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
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 ... }
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
{ // "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; } } }
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.
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 + "."); } } } }