Groovy Module


Overview

This module was created as a proof of concept (for embedding Groovy into OpenMRS) and to serve as a base module for other modules that want to use Groovy scripting as well.

Setup

Download and install the Groovy module omod file from the module repository.

Usage

Find and click on the Groovy Scripting Form link within the Groovy Module section of OpenMRS' administration page. You should be taken to a simple web form with a single textarea followed by a "GO" button. Type any valid Groovy script into the textarea and click the button to execute the script.

Some variables are automatically provided (most of these represent the API service):

  • cohort
  • concept
  • encounter
  • form
  • locale
  • logic
  • obs
  • patient
  • person
  • user

List 100 concepts?

import groovy.xml.MarkupBuilder
 
writer = new StringWriter()
b = new MarkupBuilder(writer)
 
b.table(border: 1) {
  1.upto(100) {
    c = concept.getConcept(it)
    tr { td(c.name, style:"font-weight:bold"); td(c.name.description) }
  }
}
 
println writer

Display some encounters?

p = patient.getPatient(2)
println "<h2>${p.givenName} ${p.familyName} Encounters</h2>"
for (e in encounter.getEncounters(p)) {
  println "${String.format('%tF', e.encounterDatetime)} - ${e.creator}<br />"
  println "<ul>"
  for (o in e.getObs()) {
    println "<li>${o.concept.name}</li>"
  }
  println "</ul>"
}

Display some observations

p = patient.getPatient(2)
println "<h2>${p.givenName} ${p.familyName}</h2>"
for (o in obs.getObservations(p, false)) {
  println "${o.concept.name} : ${o.getValueAsString(locale)}<br />"
}

Search for patients by name

s = "nga"
for (p in patient.getPatientsByName(s)) {
  n = "${p.givenName} ${p.familyName}".replaceAll("(?i)($s)", "<b>\$1</b>")
  println "${n}<br />"
}

Manage modules

import org.openmrs.module.ModuleFactory
 
// Uncomment the next line to start the printing module
// ModuleFactory.startModule(ModuleFactory.getModuleById("printing"))
 
// list out your running modules
for (m in ModuleFactory.startedModules) {
  println "${m.name} : ${m.started}<br />"
  // uncomment the next line to stop the printing module
  // if (m.name == "Printing") ModuleFactory.stopModule(m)
}

A more complex example

/*
 * For adult return visits within two clinic modules during November 2008, examine the CD4 counts
 * available to the clinician at the time of the encounter, calculate several decision support reminder
 * triggers, and then output -- as comma-separated values -- each visit along with whether or not each 
 * reminder would have been triggered, whether the clinician ordered a CD4 count at the visit, and whether
 * or not a CD4 was obtained during the visit (or within the following week).  Include all known CD4 counts
 * the the patient at the end of each line for quick validation.
 */
import org.openmrs.Obs
import org.openmrs.Person
import org.openmrs.Encounter
 
// Easy SQL calls
def sql(s) { admin.executeSQL(s,false) }
 
// Desired date format
def df = new java.text.SimpleDateFormat('MM/dd/yyyy')
 
// Used to compare observations -- e.g., to find max/min
def byValue = [compare:{ a, b -> a.valueNumeric <=> b.valueNumeric }] as Comparator
def byDate = [compare:{ a, b -> a.obsDatetime <=> b.obsDatetime }] as Comparator
 
// Some magic Groovy Obs methods
Obs.metaClass.'asOf' = { Date d -> delegate.obsDatetime < d }
Obs.metaClass.'above' = { value -> delegate.valueNumeric > value }
Obs.metaClass.'below' = { value -> delegate.valueNumeric < value }
Obs.metaClass.'text' = { "${delegate.valueNumeric} on ${df.format(delegate.obsDatetime)}" }
 
// Add some magic to lists (ArrayList is Groovy's default for lists) for convenience
ArrayList.metaClass.'asOf' = { Date d -> delegate.findAll{ it.asOf(d) }}
ArrayList.metaClass.'text' = { delegate.collect{it.text()} }
 
// More Groovy fun: checking list sizes in a way a doctor might understand -- e.g., 2.of(this) && 3.orMore(that)
Integer.metaClass.'of' = { list -> list != null && list.size() == delegate }
Integer.metaClass.'orMore' = { list -> list != null && list.size() >= delegate }
 
// And why not make it easy to lookup CD4s or identifiers using internal IDs while we're enjoying Groovy magic
def CD4_CONCEPT = concept.getConcept(5497) // Our CD4 concept
Integer.metaClass.'cd4s' = { obs.getObservationsByPersonAndConcept(new Person(delegate),CD4_CONCEPT) }    
Integer.metaClass.'identifiers' = { patient.getPatient(delegate).identifiers.collect{ it.identifier } }
 
// Fetch names of target locations
targetLocations = [13, 14] // 13 = Module 2, 14 = Module 3
location = org.openmrs.api.context.Context.locationService
locationName = [:]; targetLocations.each { locationName[it] = location.getLocation(it).name }
 
// Methods to check for orders or results reported within an encounter
def TESTS_ORDERED = concept.getConcept(1271) // Our TEST ORDERED concept
def CD4_PANEL = concept.getConcept(657) // Our CD4 PANEL concept (answer to TESTS ORDERED if doctor ordered a CD4)
// Return true if CD4 ordered within encounter for patient
def cd4Ordered = { pid, encId ->
  1.orMore(obs.getObservations([new Person(pid)],[new Encounter(encId)],[TESTS_ORDERED],
  [CD4_PANEL],null,null,null,null,null,null,null,false))
}
// Return true if CD4 result within one week of encounter date
def cd4Obtained = { pid, encDate ->
  1.orMore(obs.getObservations([new Person(pid)],null,[CD4_CONCEPT],
  null,null,null,null,null,null,encDate-1,encDate+7,false))
}
 
// Our reminders
def reminders = [
    // No prior CD4
    1 : { encDate, cd4s -> cd4s == null || 0.of(cd4s) },
 
    // Only one CD4 over 6 months ago
    // TODO: need to match Brian's logic for what "6 months" means
    2 : { encDate, cd4s -> 1.of(cd4s) && cd4s[0].asOf(encDate-180) },
 
    // Only two prior CD4s, at least one < 400, both over 6 months ago
    3 : { encDate, cd4s -> 2.of(cd4s) && cd4s.min(byValue).below(400) && cd4s.max(byDate).asOf(encDate-180) },
 
    // More than two prior CD4s, latest < 400 and over 6 months ago
    4 : { encDate, cd4s -> 3.orMore(cd4s) && cd4s.max(byDate).below(400) && cd4s.max(byDate).asOf(encDate-180) },
 
    // More than two prior CD4s (or only two if both >400) with most recent over 12 months ago and >400
    5 : { encDate, cd4s -> (3.orMore(cd4s) || (2.of(cd4s) && cd4s.min(byValue).above(400)))
                           && cd4s.max(byDate).asOf(encDate - 365) }
 
    ] // end of reminders
 
// Select adult return visit encounters at our target locations between 3-Nov-2008 and 28-Nov-2008
rs = sql("""
    select patient_id, encounter_id, encounter_datetime, location_id
    from encounter
    where encounter_datetime >= '2008-11-03' and encounter_datetime < '2008-11-29'
        and encounter_type = 2 /* ADULTRETURN encounter type */
        and location_id in (${targetLocations.join(',')})
""")
 
// Loop through the encounters, creating a CSV output
println "Date,Patient,pid,Encounter,Module,${reminders.keySet().join(',')},Any,Ordered,Obtained,CD4s"
rs.each { row ->
    (pid, encId, encDate, locationId) = row    
    identifier = pid.identifiers()[0]  // first patient identifier is preferred
    module = locationName[locationId]  // the clinic module
    allCd4s = pid.cd4s()               // all known CD4 counts for the patient
    cd4s = allCd4s.asOf(encDate)       // only CD4s prior to the current encounter
    ordered = cd4Ordered(pid, encId) ? 'Y' : 'N'
    obtained = cd4Obtained(pid, encDate) ? 'Y' : 'N'
    reminderResults = reminders.collect { n, fn -> fn(encDate,cd4s) ? 1 : 0 } // one line runs our reminders
    anyReminder = reminderResults.sum() > 0 ? 1 : 0
 
    // output in CSV format
    print "${df.format(encDate)},$identifier,$pid,$encId,$module,${reminderResults.join(',')},$anyReminder,"
    print """$ordered,$obtained,"${allCd4s.sort(byDate).reverse().text().join(',')}"\n"""
}

Errors

java.lang.ClassFormatError: Invalid method Code length 82342 in class file Script1

Solution: Java only allows methods that are 64K long. Reduce the size of your groovy script.