Developing Custom Data Handlers for Complex Obs

                         

Complex Obs Support gives OpenMRS the ability to store observations (Obs) on a patient with complex values such as an x-ray image, a document, or a media file.                  

                             

To promote further flexibility over which data types can be supported, we recently implemented a custom handler system.

Custom data types can be divided into two main categories

  1. Data which doesn’t refer to an external database
    1. Domain data types ( data types based on existing domain objects)

                               

●  These represent data types which can be stored in the OpenMRS database as either a reference or as an independent value

●  Domain data types don’t refer to any external database

●  Already implemented custom data types are PatientHandler, PatientFieldGenObsHandler, LocationHandler and LocationFieldGenObsHandler.

  1. Data which refers to an external file system
    1. Large data files such as jpgs or text files

                                   

  • These are objects which are too large to be stored in the database alone.
  • These objects are usually stored in the external database (in the OpenMRS Application data directory etc.).
  • Reference on how to locate these files are then stored in the OpenMRS database.

                          

To create a complex observation, you must have a complex concept in your concept dictionary.

Complex concepts may be created using the default handlers already provided in the trunk.

The only requirement to add an additional custom data type to the available list is to create a valid handler class, and have it registered by spring. You may then use it to create complex concepts and observations.

Let’s go through the necessary steps to create such a handler.

Creating a custom data handler 

  1. Handler class name must end with ‘Handler’ (for ease of identification)
  2. Handler class should be stored in the appropriate packages of the api layer or web layer. This is not a must, but is preferred.
  3. Handler class must implement ComplexObsHandler interface.

*However please note that not all classes which end with the suffix ‘-Handler’ are complex obs handlers. For example, other unrelated handlers used by OpenMRS custom tags also end with the suffix ‘-handler’.

At spring initialization time, spring searches through the source code for any spring components that implement the ComplexObsHandler interface. These are detected and loaded as valid handlers based on their priority.

These handlers are then displayed in the ConceptForm.jsp. You may now use the handler to create complex concepts and observations.

                   

To ensure that your handler will be detected and loaded, make sure that it contains the following annotations at its head.

@Component

@Order(Ordered.LOWEST_PRECEDENCE)

//Decide the order value based on your needs

         

The ComplexObsHandler Interface requires that you implement several methods. These are,

public Obs saveObs(Obs obs) throws APIException;

public Obs getObs(Obs obs, String view);

public boolean purgeComplexData(Obs obs);

public String getHandlerType();

public boolean validate(String handlerConfig, Obs obs);

               

Let’s look at PatientHandler.java as an example of how you may create a hander for a custom data type

 
/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package org.openmrs.obs.handler;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.lang.StringUtils;
import org.openmrs.Obs;
import org.openmrs.Patient;
import org.openmrs.api.APIException;
import org.openmrs.api.PatientService;
import org.openmrs.api.context.Context;
import org.openmrs.obs.ComplexData;
import org.openmrs.obs.ComplexObsHandler;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * Handler for storing Patient objects as the value for Complex Observations. 
 * The Patient Id of each Patient object is stored in the value_complex column of the Obs table. 
 * This value may be preceded by an optional displayable value. 
 * 
 * This class is the most basic level of implementation
 * for PatientHandlers. For a more improved version, see
 * org.openmrs.obs.handler.PatientHandler.PatientFieldGenObsHandler or consider extending
 * PatientHandler class to meet your requirements. 
 * 
 * There may be several classes which extend PatientHandler. 
 * Out of these, only one will be loaded by Spring. The class to be loaded will be
 * decided based on the @Order annotation value. As default, PatientHandler will have the lowest
 * possible priority, and will be overridden by PatientFieldGenObsHandler, which lets us use
 * fieldGen tags for data entry.
 */

@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class PatientHandler extends CustomDatatypeHandler implements ComplexObsHandler {

 public static final Log log = LogFactory.getLog(PatientHandler.class);

 /** The Constant HANDLER_TYPE. Used to differentiate between handler types */
 public static final String HANDLER_TYPE = "PatientHandler";

 /** The Constant DISPLAY_LINK. Used as a link to display the selected patient */
 public static final String DISPLAY_LINK = "/patientDashboard.form?patientId=";

 public PatientHandler() {
 super();
 }

 /**
  * This method is used to save the Obs. Firstly, the selected Patient Instance is retrieived.
  * then, the valueComplex String to be persisted is created based upon this instance.
  * The valueComplex String is created via a call to a method in Obs.java
  * 
  * @see org.openmrs.obs.ComplexObsHandler#getObs(org.openmrs.Obs, java.lang.String)
  */
 @Override
 public Obs saveObs(Obs obs) throws APIException {
 PatientService ps = Context.getPatientService();

 Patient patient = ps.getPatient(Integer.parseInt(obs.getComplexValueKey()));

 if (patient == null) {
 throw new APIException("Cannot save complex obs where obsId=" + obs.getObsId() + " Desired Patient id :"
         + Integer.parseInt(obs.getComplexValueKey()) + " cannot be found");
 }

 obs.setComplexValueText(patient.getPersonName() + "(" + patient.getPatientIdentifier() + ")");

 // Retreive complexValueText and complexValueKey values
 String complexValueText = obs.getComplexValueText();
 String complexValueKey = obs.getComplexValueKey();

 obs.setValueComplex(complexValueText, complexValueKey);
 // Remove the ComlexData from the Obs
 obs.setComplexData(null);

 return obs;
 }

 /**
  * This method retrieves the patient instance persisted in the database. The patient is retrieved
  * using the ComplexValueKey. It is passed into ComplexData object, and returned with the
  * Obs.
  * 
  * @see org.openmrs.obs.ComplexObsHandler#saveObs(org.openmrs.Obs)
  */
 @Override
 public Obs getObs(Obs obs, String view) {
 Patient patient = null;
 PatientService ps = Context.getPatientService();

 String key = obs.getComplexValueKey();

 if (key != null && !StringUtils.isEmpty(key))
 patient = ps.getPatient(Integer.parseInt(key));

 if (patient != null) {
 ComplexData complexData = new ComplexData(obs.getComplexValueText(), patient);
 obs.setComplexData(complexData);
 } else
 log.info("Warning : specified patient cannot be found - returning no ComplexData for " + obs.getObsId()
         + " Is this to be used for editing purposes ?");

 return obs;
 }

 /**
  * @see org.openmrs.obs.ComplexObsHandler#purgeComplexData(org.openmrs.Obs)
  */
 @Override
 public boolean purgeComplexData(Obs obs) {
 // Default value for now.
 return true;
 }

 /**
  * Gets the link used to display a selected patient to an user.
  * 
  * @return the display link
  */
 public String getDisplayLink() {
 return PatientHandler.DISPLAY_LINK;
 }

 /**
  * Gets the handler type for each registered handler.
  * 
  * @return the handler type
  */
 public String getHandlerType() {
 return PatientHandler.HANDLER_TYPE;
 }

 /**
  * Validate.
  */
 @Override
 public boolean validate(String handlerConfig, Obs obs) {
 //Default for now
 return true;
 }

 /**
  * This method is used to return the persisted data only. The Patient instance is retreived
  * using data from the Obs passed in. This instance is returned to the user. If there is no
  * matching patient, then the method returns null.
  */
 @Override
 public Object getValue(Obs obs) {
 Patient patient = null;
 if (obs.getValueComplex() != null && !StringUtils.isEmpty(obs.getValueComplex())) {
 PatientService ps = Context.getPatientService();
 patient = ps.getPatient(Integer.parseInt(obs.getComplexValueKey()));

 return patient;
 } else {
 return null;
 }
 }
}
 

           

●      The getObs and saveObs methods deal with the storage / retrieval of data.

●      The saveObs methods’ job is to take your data, and find a way to represent it in the database.

In case of PatientHandlers, the reference value stored in the database (in obs.valueComplex) is in the format of -

Value display text | identifier

             

For each obs, valueComplex must contain a user friendly display value and an identifier (key) separated by a delimiter. As an example, the valueComplex for a patient would look as follows,

'Mr. John D Patient(100-8)|3'

                                 

The getObs method takes the reference value stored in obs.valueComplex, and transforms it back into the data originally stored by the user.

The method obsValue(Obs) is used  to return the complex data object only. It takes the Observation, retrieves the complex data stored in it, and returns it as an object.

This method is essential to work with fieldGen tags.

In other cases, the method returns null.

Also note the variable ‘displayLink’.

Let’s assume that your handler is for a custom domain object. If so, chances are that there already exists a dashboard or form which is the best way to display the given object.

The aim of the displayLink variable is to give a developer who created the handler the opportunity to define the best way to view an observation value created using this handler.

For example, the default displayLink for PatientHandler leads to the PatientDashboard.

However, this value applies only to custom data types (handlers representing domain classes)

 

Registering the your module's handler

The code below is added to the moduleApplicationContext.xml file, if it is a web based handler, add it to webModuleApplicationContext.xml instead

<bean parent="obsServiceTarget" >
    <property name="handlers">
        <map>
            <entry>
                <key><value>patientHandler</value></key>
                <bean class="org.openmrs.obs.handler.PatientHandler"/>
            </entry>
        </map>
    </property>
</bean>

 

                    

PLEASE NOTE: A lot depends on the HANDLER_TYPE Constant defined in each handler class. If your custom data type is unique, and does not extend a core handler already in your system, then make sure that the HANDLER_TYPE value is unique, and lets you recognize your handler easily.

On the other hand, if your new handler extends an existing handler class, then please don’t attempt to set a new HANDLER _TYPE value.