Example batch process for unit of work processing

The following Gosu code is an example of a type of a batch process that processes units of work rather than operating as a background task. Its process history tracks the number of items that were processed successfully or that failed. Its units of work are urgent activities.

The batch process implements a notification scheme for urgent activities such that:

  • The activity owner must respond to an urgent activity within 30 minutes.
  • If the activity owner does not handle the issue within that time limit, the batch process notifies a supervisor.
  • If the supervisor fails to resolve the issue in 60 minutes, the batch process sends a message further up the supervisor chain.

If there are no qualified activities, the batch process returns false so that it does not create a process history. If there are items to handle, the batch process increments the count. The application uses this count to display batch process status as "n of t" or a progress bar. If there are no contact email addresses, the task fails and the application flags it as a failure.

This example checks the value of the TerminateRequested flag to terminate the loop if the user or the application requested to terminate the process.

Notice also, that if the a TestBatch typecode on the BatchProcessType typelist does not exist, you must create it.

The code in this Gosu example does not actually send the email. Instead, the code prints the email information to the console. You can change the code to use real email APIs.

package sample.processes

uses gw.api.database.Query
uses gw.api.email.Email
uses gw.api.email.EmailContact
uses gw.api.email.EmailUtil
uses gw.api.profiler.Profiler
uses gw.api.profiler.ProfilerTag
uses gw.api.util.DateUtil
uses gw.processes.BatchProcessBase
uses java.lang.StringBuilder
uses java.util.HashMap
uses java.util.Map

class TestBatch extends BatchProcessBase {
  static var tag = new gw.api.profiler.ProfilerTag("TestBatchTag1")

  //If BatchProcessType typcode TestBatch does not exist, you must create it.
  construct() {
    super( BatchProcessType.TC_TESTBATCH )
  }

  override function requestTermination() : boolean {
    super.requestTermination() // set the TerminationRequested flag
    return true // return true to signal that we will attempt to terminate in our doWork method
  }

  override function doWork() : void { // no bundle
    var frame = Profiler.push(tag)

    try {    
      var activityQuery = Query.make(Activity)
      activityQuery.compare(Activity#Priority, Equals, Priority.TC_URGENT)
      activityQuery.compare(Activity#Status, Equals, ActivityStatus.TC_OPEN)
      activityQuery.compare(Activity#CreateTime, LessThan, DateUtil.currentDate().addMinutes(-30))
      OperationsExpected = activityQuery.select().getCount()
      var map = new HashMap<Contact, StringBuilder>()

      for (activity in activityQuery.select()) {
        if (TerminateRequested) {
          return
        }
        incrementOperationsCompleted()
        var haveContact = false
        var msgFragment = constructFragment(activity)
        haveContact = addFragmentToUser(map, activity.AssignedUser, msgFragment) or haveContact
        var group = activity.AssignedGroup

        if (activity.CreateTime < DateUtil.currentDate().addMinutes( -60 )) {
          while (group != null) {
            group = group.Parent
            haveContact = addFragmentToUser(map, group.Supervisor, msgFragment) or haveContact
        }

      }

      if (!haveContact) {
        incrementOperationsFailed()
         addFragmentToUser(map, User.util.UnrestrictedUser, msgFragment)
       }
     }

    if (not TerminateRequested) {
      for (addressee in map.Keys) {
        sendMail(addressee, "Urgent activities still open", map.get(addressee).toString())
      }
    } finally {
      Profiler.pop(frame)
    }
  }

  private function constructFragment(activity : Activity) : String {
    return formatAsURL(activity) + "\n\t" + " Subject: " + activity.Subject + " AssignedTo: " 
          + activity.AssignedUser + " Group: " + activity.AssignedGroup + " Supervisor: " 
          + activity.AssignedGroup.Supervisor + "\n\t" + activity.Description
  }

  private function formatAsURL(activity : Activity) : String {
    // TODO: YOU MUST ADD A PCF ENTRYPOINT THAT CORRESPONDS TO THIS URL TO DISPLAY THE ACTIVITY.
    return "http://PolicyCenter:8180/pc/Activity.go(${activity.ID})
  }

  private function addFragmentToUser(map : HashMap<Contact, StringBuilder>, user : User, 
        msgFragment : String) : boolean {
    if (user != null) {
      var email = user.Contact.EmailAddress1

      if (email != null and email.trim().length > 0) {
        var sb = map.get(email)

        if (sb == null) {
          sb = new StringBuilder()
          map.put(user.Contact, sb)
        }

        sb.append(msgFragment)
        return true
      }
    }
    return false
  }

  private function sendMail(contact : Contact, subject : String, body : String) {
    var email = new Email()
    email.Subject = subject
    email.Body = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n" +
                 "<html>\n" +
                 "  <head>\n" +
                 "    <meta http-equiv=\"content-type\"\n" + "content=\"text/html; charset=UTF-8\">\n" +
                 "    <title>${subject}</title>\n" +
                 "  </head>\n" +
                 "  <body>\n" +
                 "    <table>\n" +
                 "      <tr><th>Subject</th><th>User</th><th>Group</td><th>Supervisor</th></tr>" +
                 "    </table>" +
                 "  </body>" +
                 "</html>" 
    email.addToRecipient(new EmailContact(contact))
    EmailUtil.sendEmailWithBody(null, email);
  }
}

To use this entry point, create the following PCF entry point file with name Activity.

<PCF>
  <EntryPoint authenticationRequired="true" id="Activity" location="ActivityForward(actvtIdNum)">
    <EntryPointParameter locationParam="actvtIdNum" type="int"/>
  </EntryPoint>
</PCF>

In Studio, create this Activity.pcf file in the following location: configuration > config > Page Configuration > pcf > entrypoints

Also include the following ActivityForward PCF file as the ActivityForward.pcf in each place in the PCF hierarchy that you need it:

<PCF>
  <Forward id="ActivityForward">
    <LocationEntryPoint signature="ActivityForward(actvtIdNum : int)"/>
    <Variable name="actvtIdNum" type="int"/>
    <Variable name="actvt" type="Activity">initialValue="find(a in Activity 
          where a.ID == new Key(Activity, actvtIdNum))"</>
    <ForwardCondition action="PolicyForward.go(actvt.Policys);
    ActivityDetailWorksheet.goInWorkspace(actvt)"/>
  </Forward>
</PCF>
Important: You must use the runWithNewBundle transaction method to modify entity data in your custom batch process. For more information, see Running code in an entirely new bundle.