Software Development: Exploring OpenMRS

[article] [edit page] [discussion] [history]

From Humanitarian-FOSS Project Development Site

THESE NOTES ARE PRELIMINARY---THERE MAY BE INCONSISTENCIES AND INACCURACIES.

Contents

Read the OpenMRS Technical Overview

The OpenMRS Technical Overview gives a quick introduction to a number of the technologies used by OpenMRS. You may not understand everything on this page, but read through it anyway.

Adding some data to the database

The sample database that is provided with OpenMRS has many patients and concepts entered (see below for more on the latter), but no observations. Here we'll set things up so you can add encounters and observations; you should see the changes you make in the application reflected in the openmrs database.

  1. Add a provider. A provider is someone who can add observations to patients; without any providers, we cannot add encounters and observations. We could make the OpenMRS administrator a provider, but that seems unreasonable, so instead we'll make a new account.
    1. From the Administration page, select "Manage Users."
    2. On the "User Management" page, select "Add User."
    3. On the "Adding a User" page, enter a name (maybe your name), birthdate, and sex, and press "Create Person."
    4. On the "User Form" page, fill in the fields, and be sure to select the "Provider" role. Press "Save User."
    5. Stay logged in as admin (I'm not sure why, yet).
  2. Add an encounter and an observation.
    1. From the "Administration" page, select "Manage Encounters."
    2. On the "Encounter" page, select "Add encounter."
    3. Press the "Select" for the Patient. In the text box, enter "oloo". This will automatically bring up all patients such that "oloo" matches some part of the name (in my version of the database, there is only one such patient, Anet Wanza Oloo). Select such a patient.
    4. Press the "Select" button for Provider. Select the user you created in the previous step.
    5. Choose "Unknown location" for the location, enter a date, and choose "Basic form" for the Form.
    6. Press "Save encounter." This redisplays the current page with the same data that you just entered. Now select "Add observation."
    7. Press the "Select" button for Concept. Type "chest" into the text box and select "X-RAY, CHEST." Press the "Select" button for Value and select "NORMAL."
    8. Press "Save observation."

The Data Model

This explanation of the data model is cribbed from a presentation that Darius gave us at the beginning of the semester.

The most straightforward approach to the data model for an EMR system is to have tables that correspond to events; the fields of these tables correspond to data collected or observations made. For example, there might be an visit table in which each row represents a visit by a patient:

patient_id date weight height num_children home_type
101-Oct-2005871542"Mud"
203-Oct-2005951821"Cinder-block"
114-Aug-2007981543"Wood-frame"

The data recorded here is data that might change visit by visit. Presumably there would be a patient table in which patient identifiers are associated with unchanging data (or at least data for which only the current value is ever of interest) associated to that patient. The problem with this approach is that it is not easily extensible. What happens if there is new information that should be collected after the application is deployed? This involves changes to the database (inconvenient), making up values for entries already present in the database (possibly misleading), and probably changing application and presentation logic (which may make assumptions about what fields are present in the table).

Instead, OpenMRS makes use of a concept dictionary; this is a listing of all the concepts that might be used in other parts of OpenMRS. For example, "weight" and "home type" might be concepts. A concept is specified in the concept table by a name, a data type (e.g., integer or string), and other information. The concept table itself gives numeric identifiers to the concepts; other parts of the specification (e.g., the data type) are specified as identifiers in other tables (e.g., concept_datatype). Let's look at a particular example. In the concept table we see the following row:

concept_iddatatype_idclass_id
1221

There are other fields in this table, mostly having to do with administrative information such as who created the concept, whether it is a concept that is currently used, etc. From concept_name we find the following:

concept_idname
12X-RAY, CHEST

So this concept refers to a chest x-ray. From concept_class we find the following:

concept_class_idnamedescription
1TestAcq. during patient encounter (vitals, labs, etc.)

So it looks like this concept refers to a test that is performed on the patient. From concept_datatype we find

concept_datatypename
2Coded

which indicates that the possible values that can be recorded for an observation of this concept come from a fixed list of values. We can find out what the possible values are by looking at the concept_answer table for rows with concept_id equal to 12:

concept_answer_idconcept_idanswer_concept
1121115
2121136
3121137
4125158
5125622
6126049
7126050
8126052

Notice that each answer_concept value is a concept_id in the concept table! Chasing this through, we see that concept 1115 has name NORMAL and no associated datatype (the name says it all...).

So far all we have done is to define concepts. How do they get used? One of the other main tables in the OpenMRS data model is the obs table. Each row in this table represents an observation of a person. One of the fields is concept_id, identifying which concept is associated to this observation. The values for this particular observation are stored in other fields for the row. For example, if the associated concept is of Coded datatype, then the actual code (value) for the observation is recorded as the value of the value_coded field (which, of course, will be a concept identifier!). Notice that there are many value_* fields in the obs table---the only field to receive a non-null value will be the field that corresponds to the concept's datatype. If you have added an encounter and an observation as in the previous section, you should see a single row in this table.

Adding a concept to OpenMRS

What happens when we add a concept to OpenMRS? Here's how we find out.

  1. If we go to the Concept Dictionary Maintenance page, we see that the "Add new Concept" link requests the URL dictionary/concept.form (relative to the webapp).
  2. We look at the web application context file web/WEB-INF/openmrs-servlet.xml to see how the URL is mapped. We find
    <bean id="urlMapping" class="...SimpleUrlHandlerMapping">
        <property name="interceptors">
            <list>
                <ref bean="localeChangeInterceptor" />
                <!-- <ref bean="openSessionInViewInterceptor"/> -->
    
            </list>
        </property>
        <property name="order"><value>99</value></property>
        <property name="mappings">
            <props>
    
            ...
                <prop key="dictionary/concept.form">conceptForm</prop>
            </props>
        </property>
    </bean>
    

    This tells us that the request for dictionary/concept.form

    is handled by the bean named conceptForm. We now look for that bean definition in openmrs-servlet.xml:

    <bean id="conceptForm" 
            class="org.openmrs.web.controller.ConceptFormController">
        <property name="commandName"><value>concept</value></property>
        <property name="validator">
    
            <ref bean="conceptValidator" />
        </property>
        <property name="formView"><
            value>/dictionary/conceptForm</value>
        </property>
    
        <property name="successView">
            <value>concept.form</value>
        </property>
    </bean>
    

    So now we know that "Add new Concept" will be handled by ConceptFormController.

  3. We now look at ConceptFormController.java and see that it extends Spring's SimpleFormController. That's good, because this is well-documented. Reading about SimpleFormController, we learn:
    • The page used to display the form is /dictionary/conceptForm; the jspViewResolver bean will map this to /WEB-INF/view/dictionary/conceptForm.jsp, so that is where we look to find the JSP for this form.
    • On success, we will request concept.form again, presumably to add another concept.
    • The validator for the data that is submitted on this form is defined by the conceptValidator bean; look through openmrs-servlet.xml to find this bean definition.
    • The command name for the form view is concept, so we expect the form view to refer to ${concept...}, an object that is likely to be populated by the formBackingObject method of SimpleFormController, which in turn is probably overridden by ConceptFormController.
  4. Now looking at ConceptFormController.java, we see that it overrides the following methods from SimpleFormController: initBinder, processFormSubmission, onSubmit, formBackingObject, and referenceData. It now behooves us to read about SimpleFormController to understand what each of these methods does, and how they are used in the SimpleFormController workflow. Noticing that processFormSubmission seems focused on examining the submission to look for certain kinds of errors, we focus on onSubmit.
    1. ConceptService cs = Context.getConceptService(): we get a ConceptService object that presumably allows us to manipulate concepts in the database.
    2. Next we see a main conditional that tests Context.isAuthenticated(); we assume (we should eventually verify) that this is true if the user has the necessary credentials to use this context.
    3. Concept concept = (Concept)obj: cast the command object from the form to a Concept object.
    4. The next main conditional is of the form
      if (action.equals(msg.getMessage("Concept.delete"))) {
      	...
      }
      else {
      	...
      }
      

      Again, we make some educated guesses, which we should eventually verify: this tests whether the action on the form was to delete a concept or not. We're considering the latter case, so we pay attention only to the else block.

    5. String isSet = ServletRequestUtils.getStringParameter(request, "conceptSet", "");. Here we can make an educated guess that this gets the value of some attribute of the request. But if that's what is happening, then we need to look at the form to see how that attribute (probably conceptSet) gets set, and in that case we had better make sure we know what this call does instead of making an educated guess. We look at the import statements and see that ServletRequestUtils is a Spring class. The API documentation is good first place to look. We notice that the description of this class indicates that this is a good class to use for simple submissions where the use of a command object would be overkill and that yes, getStringParameter does just what we expect (along with supplying a default value to use as the return value if the parameter was not set in the form).
    6. Now we look at conceptForm.jsp and search for conceptSet. We find it as the name of an attribute in the following context:
      <spring:bind path="concept.set">
      	<input type="checkbox" name="conceptSet" id="conceptSet" 
      		<c:if test="${status.value}">
      			checked="checked"
      		</c:if> 
      		onClick="changeSetStatus(this)" />
      
      	<c:if test="${status.errorMessage != ''}">
      		<span class="error">
      		${status.errorMessage}</span></c:if>
      </spring:bind>
      

      Reading about the input element in HTML, we see that

      conceptSet will presumably be set if this checkbox is selected. To be certain, we should be running OpenMRS and see if there is a corresponding checkbox on the "Add new concept" form. There is, and selecting the checkbox immediately produces another collection of controls. In our current analysis, we have to decide whether we want to consider that situation or not; it seems simplest to assume that it is not checked. Then the question is what the value of this control will be. There is no value attribute, so this is probably handled by some sort of default. What is strange is that the documentation for the input control says that the value attribute is mandatory for checkboxes. I'm not sure what is going on here. However, we do know that if the checkbox is not selected, then this control is not successful, and so conceptSet will not be one of the parameters in the form submission. So in this case, isSet will get the value "".

      However, since this input element occurs inside a spring:bind element, we need to read up on those to see what else is happening here. For that, we probably look at the Spring Reference manual, Chapter 14. Unfortunately, we don't find too much helpful there; the only reference to something like spring:bind is in the section on Velocity and Freemarker, and that seems to be alternative syntaxes. The Appendix of the Spring Reference manual does give a definition of the spring:bind element, but it is not very helpful. However, looking at the discussion for Velocity and Freemarker, it seems that this element probably arranges so that the value of the input element that is contained by the spring:bind element is bound to the concept.set path in the command object. We can at least see if this is reasonable by checking whether there is a set attribute of the command object for this controller. And indeed, when we look at Concept.java (the type of the command object), there is a boolean field set, which matches up with the input type being a checkbox. It is odd, though, that if isSet gets any value other than "", then onSubmit "manually" sets the set property of the concept; it seems that this is what the spring:bind tag ought to be doing.

    7. if (concept.getConceptId() == null) {...} else {...}. It seems reasonable that the concept ID is not set, as we are just now adding a new concept. But, we should be sure. First we look at the command object created by formBackingObject to see whether it is set or not there, and we see that if the conceptId parameter of the request for this form is not set, then the conceptId property of the command object will be left unset. Again, this seems reasonable, but really we should do some debugging and verify this; we could either add some log messages or we could use Eclipse's debugging facilities. For now, we'll make it an assumption.
    8. concept.setConceptId(cs.getNextAvailableId()). Hopefully this is self-explanatory.
    9. if (...) { concept = getConceptNumeric(...) }. If the guard is true, we will make this concept into a Numeric concept, one of the special-case concepts in OpenMRS.
    10. cs.saveConcept(concept). Finally save the concept to the database. We could pursue this by examining ConceptService.java; eventually we should see the data being saved. But we'll stop here...
Personal tools