Difference between revisions of "Java Code Samples"
imported>Aeric |
imported>Aeric |
||
Line 707: | Line 707: | ||
# Modify the Form for any object in which you want to add Followers: | # Modify the Form for any object in which you want to add Followers: | ||
#: Add a checkbox and OnClick JavaScript code to add and remove records from the Followers object. | #: Add a checkbox and OnClick JavaScript code to add and remove records from the Followers object. | ||
#: When adding a | #: Create a record-updated rule that runs only when that field has changed. (Hint: Use an ''Expression'' condition.) | ||
#: | #: When adding a follower, copy the the current User's email address to the <tt>email_address</tt> field. | ||
#: To delete a follower, invoke a method that searches for a record with a matching email_address and related_to field. (Cannibalize the code below) | |||
# Add a Username field to the Followers object | # Add a Username field to the Followers object | ||
#: Automatically populate it using the current User's information, when adding a record | #: Automatically populate it using the current User's information, when adding a record |
Revision as of 03:00, 2 October 2014
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.
- <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 {
public void log(String msg) throws Exception { // Put the message in the log. Logger.info(msg, "Your Class"); } 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); }
public void doSomething(Parameters p) throws Exception { try { debug("Started"); 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 = "Something failed"; log(msg); Functions.throwError(msg + "\n" + r.getMessage() ); } debug("Success"); } 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"); } }
} // 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.
Additional Notes:
- Object names and IDs
- Most APIs take either object name or object ID. (Only a few require object ID.)
- For most operations you'll know which object you're operating on, so you'll specify the object name in a string.
- But the object ID is also available in the incoming Parameters, when you need it.
- List of Parameters
- Add this line to the code above to put a complete list of incoming parameters into the debug log:
- <syntaxhighlight lang="java" enclose="div">
// Generate a newline-separated list of parameters debug( p.toString().replace(",", ",\n") ); </syntaxhighlight>
3 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
4 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.
4.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>
4.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()
4.3 Learn more
5 Manage Files
Files can be added to a specific field in a record, or added to the list of attachments.
5.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>
5.2 Add and Access Record Attachments
5.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>
5.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>
5.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>
6 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 * 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>
7 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
7.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:
7.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.
- <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 attachment here break; } } } }
</syntaxhighlight>
8 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).
9 Send Notification Messages to Followers
This code sample sends an email to everyone who registered themselves as a "follower" of a Case. (To follow the record, a entry with their email address is added to the Followers object.) When the Case is updated, an email notification is sent to everyone that list.
- Learn more:
- Related Records
- Email Templates
- SendEmailUsingTemplate (Java API)
9.1 Setup and Testing
- Use the Object Construction Wizard to create the Followers object and define an Email Address 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.)
- Modify the Case form to create a new Related Information section that displays Follower records.
- Select the Followers object
- Link the Follower's Related To field to the object's ID field
- Select Email Address as the field to display
- Sort by email address, in ascending order.
- Open a Case record and use that section to manually add yourself as a follower.
- Use the sample below to create an Email Template that will be used send notifications.
(The sample includes the most recent note added to the record history.) - Record the Template ID:
- While viewing the list of email templates, click the wrench icon and select Edit this View.
- Move the Record ID to the list of selected fields.
- Confirm that you want to make this a global change.
- That value is the Template ID. It now appears as a column in the list of views, where it can be copied.
- Create a class using the code below. Fill in the Template ID.
- 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 (Notifications, notifyFollowers)
- Update the case record and check your inbox for the notification message.
9.2 Next Steps
After testing the behavior:
- Modify the Form for any object in which you want to add Followers:
- Add a checkbox and OnClick JavaScript code to add and remove records from the Followers object.
- Create a record-updated rule that runs only when that field has changed. (Hint: Use an Expression condition.)
- When adding a follower, copy the the current User's email address to the email_address field.
- To delete a follower, invoke a method that searches for a record with a matching email_address and related_to field. (Cannibalize the code below)
- Add a Username field to the Followers object
- Automatically populate it using the current User's information, when adding a record
- Display it in the Related Information section of forms for objects that can be followed.
- For extra credit, write a method that examines the last activity in the record history, and identifies the type of activity that occurred. Then send a notifications only when the status changed, for example.
- (See the #Add and Access Notes section forthe structure of records in the Notes object.)
9.3 Email Template
- Name: Update Notification
- 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 source code 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 $Tests.modified_id.full_name
$__current_note__.description
Case Description:
$Tests.description
</syntaxhighlight>
9.4 Code
- <syntaxhighlight lang="java" enclose="div">
</syntaxhighlight>
10 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.
10.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>
10.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>
10.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:
- <syntaxhighlight lang="javascript" enclose="div">
// 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
* .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);
}; </syntaxhighlight>
11 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.
11.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>
11.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>
11.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>
11.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>