OpenMRS Developers' Guide

Creating Your First Module

Amani Clinic Case Study

To put this book in perspective, we'll walk through a fictional scenario that reflects the real world process of identifying the need for, designing, and building a module. Maintaining a modular architecture allows developers to add and remove special functionality into OpenMRS without having to modify the core project.

Overview

Let's suppose there is an established health clinic called Amani Clinic in East Africa with a few staff members who are experienced in ICT and medical informatics. They are very talented and very busy. You find and read a ticket they created when they realized functionality for adding, editing, saving, and listing of departments would be helpful to their implementation. For the purposes of this task, we will define a department as a hospital ward designed to perform a specific purpose.

Someone has left a comment "This would be a good module project!" on the ticket. You would love a new project to work on and happen to have an interest in the larger issue of "departments" as they relate to health informatics, so you are interested in taking up this task.

You send an email to Claudine, the clinic's lead informatics person and ticket requester, expressing your interest and asking for some clarification about how a user would interact with the proposed Departments module. It is a good idea to touch base with the ticket requesters or would-be users before starting a project whose scope is likely more work and background than a straightforward bug-fix. While you wait for a reply (you might not know where in the world Claudine lives), you decide to catch up on some emails of your own. You subscribed to the developers' mailing list a few days ago because you want to stay informed about what other people are working on and what issues they run into. You are excited about learning and watch a few OpenMRS University recordings too.

Claudine is enthusiastic about you building this module for their clinic and potentially many others. She invites you to keep in touch as you go along. You have already worked your way through the Getting Started chapter, so you get to work using the OpenMRS SDK to start building your module.

You officially begin work on the module by clicking on the button to 'claim' the ticket on the OpenMRS JIRA. 

Pre-development issues

While you may be ready to begin development right away, it is important to take a step back and plan out your work. Will this be a module which may be useful to all OpenMRS implementers, or implementers of the Amani clinic only? This factor may decide where the completed module will be hosted, and what kind of requirements/standards you need to maintain.

It is best not to begin development until you have discussed your design plans with other community developers, and made sure that your plans meet our requirements. 

Creating a Basic Module

The OpenMRS SDK, as explained in the Getting Started chapter, allows you to get started with a basic module in a few minutes. The omrs-create-module command helps you execute the maven archetype, creating a module directory with a framework of all the necessary module files by prompting you for specific information.

However, in order to execute this command, we assume that you're using JDK 1.7. No other pre-requisites are necessary.

To complete the omrs-create-module workflow, you will be prompted to enter the following data:

  • Module name: Enter the name of your module as you would like it to appear. For this example use the name Hello World.
  • "Do you want to add an admin page link (y/n)" : Yes, most likely you want your module to be accessible from the OpenMRS admin page.
  • "Do you want to add a service layer (y/n)": Yes, the default, creates a module with a Service, ServiceImpl, DAO, HibernateDAO, POJO, Hibernate mapping, liquibase.xml and sqldiff.xml, which will work together to allow your module to access the OpenMRS database.

Entering this data and clicking <enter> will allow the OpenMRS SDK to complete the module creation for you.

Basic Module structure 

The omrs-create-module command creates the basic module structure and components that it requires for use. Below is detailed overview of these components, their structure and how they can be used.

  • api - non-web-specific 'maven module' project
    • src
      • main - Java files in the module that are not web-specific.  These will be compiled into a distributable mymodule.jar
    • target - folder built at runtime that will contain the distributable jar file for the module
  • omod
  • pom.xml - Maven build file.  Delegates to pom.xml files in the omod and api project

You can read more about module conventions at: https://wiki.openmrs.org/display/docs/Module+Conventions 

Compiling your module 

The basic module structure comes ready to be compiled and installed onto the OpenMRS framework. To do this, navigate into the helloworld directory and execute the following command:

 mvn clean install   

This creates a jar file, and then package that jar into a omod file. The omod file is what you need to care about. It will be named basicexample-1.0-SNAPSHOT.omod, and located under the helloworld/omod/target/ folder. The omod file is the module binary, which you will install into your OpenMRS application.

Executing the maven clean install command also runs any unit tests. If you want to skip unit tests, use the following command instead:

mvn clean install -Dmaven.test.skip=true 

Try out your module

To install your module go to the Admin interface of OpenMRS.

  1. Go to http://localhost:8080/openmrs/admin/index.htm
  2. On the right side, is a Modules section. Click the Manage Modules link.
  3. Near the top, you will see an Add or Upgrade Module button, click it.
  4. Under the Add Module heading, click the Browse... button.
  5. In the file browser, select your omod file from basicexample/omod/target/basicexmaple-1.0-SNAPSHOT.omod
  6. Click Upload.
  7. You should now see your module under the Manage Modules heading.

Another alternative would be to drop the compiled omod file into the ~/.OpenMRS/modules folder.  (Where ~/.OpenMRS is assumed to be the Application Data Directory that the running openmrs is currently using.)  After putting the file in there simply restart OpenMRS and the module will be loaded and started.

When you navigate back to the main Administration page, you should see your module listed with a Basic Example Module heading, and a single sub-option of Manage module.

Customize your module 

Now that you have a basic module running, you want to add your own features which would allow it to Hello World. Where to start?

Add a new field to your data model

Let's assume that your hello world task involves adding a new field titled 'name' to your data model. 

In department/api/src/main/java/org/openmrs/module/department/Department.java, add new fields called name and description along with appropriate getters and setters for them. The file should now look as follows: 

public class Department extends BaseOpenmrsObject implements Serializable {
   private static final long serialVersionUID = 1L;
   private Integer departmentId;
   private String name;
   private String description;
   public Integer getDepartmentId() {
           return departmentId;
   }
   public void setDepartmentId(Integer departmentId) {
           this.departmentId = departmentId;
   }
   @Override
   public Integer getId() {
           return getDepartmentId();
   }
   @Override
   public void setId(Integer id) {
           setDepartmentId(id);
   }
   public String getName() {
           return name;
   }
   public void setName(String name) {
           this.name = name;
   }
   public String getDescription() {
           return description;
   }
   public void setDescription(String description) {
           this.description = description;
   }
}

Update Hibernate ORM file to work with your new field

In department/api/src/main/resources/Department.hbm.xml, uncomment the central block of code add new properties as shown below anywhere in the file. This lets Hibernate knows about the name and description fields you just created. Your file should look like the following:

 <?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" >
<hibernate-mapping package="org.openmrs.module.department">
        <class name="Department"
                table="${project.parent.artifactId}_Department">
                <id name="departmentId" type="int" column="department_id" unsaved-value="0">
                        <generator class="native" />
                </id>
                <discriminator column="department_id" insert="false" />
                <property name="uuid" type="java.lang.String" column="uuid" length="38" unique="true" />
                <property name="name" type="java.lang.String" column="name" length="255" unique="true" />
                <property name="description" type="java.lang.String" column="description" length="255" /> 
        </class>
</hibernate-mapping>

To reflect this change in the existing database, add an appropriate change set into the department/api/src/main/resources/liquibase.xml. This is the code that actually changes the database for your project to reflect your name field. A sample changeset will generally look like this:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog/1.9"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog/1.9
                  http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-1.9.xsd">
    <!--
        See http://www.liquibase.org/manual/home#available_database_refactorings
        for a list of supported elements and attributes
    -->
    <changeSet author="yourname" id="20131010-1">
        <comment>Create the department table</comment>
        <createTable tableName="department_department">
            <column autoIncrement="true" name="department_id" type="int">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="name" type="varchar(255)" />
            <column name="description" type="varchar(255)" />
            <column name="uuid" type="char(38)" />
        </createTable>
    </changeSet>
</databaseChangeLog>

Modify DAO and service layer classes support end to end interactions

The Module Maven Archetype or SDK option to add a service layer gives the module four files that make up the service layer: DAO (data access interface), HibernateDAO, Service, and ServiceImpl. The HibernateDAO is home to the sessionFactory, which actually connects to the database. The ServiceImpl will instantiate a DAO and then the module controller is free to instantiate a Service. Here is the code you will add to each file:

DAO:

/**
 * Database methods for {@link DepartmentService}.
 */
public interface DepartmentDAO {
        /**
         * @see org.openmrs.module.department.api.DepartmentService#getAllDepartments()
         */
        List<Department> getAllDepartments();
        /**
         * @see org.openmrs.module.department.api.DepartmentService#getDepartment(java.lang.Integer)
         */
        Department getDepartment(Integer departmentId);
        /**
         * @see org.openmrs.module.department.api.DepartmentService#saveDepartment(org.openmrs.module.department.Department)
         */
        Department saveDepartment(Department department);
        /**
         * @see org.openmrs.module.department.api.DepartmentService#purgeDepartment(org.openmrs.module.department.Department)
         */
        void purgeDepartment(Department department);
}

HibernateDAO:

/**
 * The default implementation of {@link DepartmentDAO}.
 */
public class HibernateDepartmentDAO implements DepartmentDAO {
        protected final Log log = LogFactory.getLog(this.getClass());
        private SessionFactory sessionFactory;
        /**
         * @param sessionFactory the sessionFactory to set
         */
        public void setSessionFactory(SessionFactory sessionFactory) {
                this.sessionFactory = sessionFactory;
        }
        /**
         * @return the sessionFactory
         */
        public SessionFactory getSessionFactory() {
                return sessionFactory;
        }
        /**
         * @see org.openmrs.module.department.api.db.DepartmentDAO#getAllDepartments()
         */
        @Override
        public List<Department> getAllDepartments() {
                return sessionFactory.getCurrentSession().createCriteria(Department.class).list();
        }
        /**
         * @see org.openmrs.module.department.api.DepartmentService#getDepartment(java.lang.Integer)
         */
        @Override
        public Department getDepartment(Integer departmentId) {
                return (Department) sessionFactory.getCurrentSession().get(Department.class, departmentId);
        }
        /**
         * @see org.openmrs.module.department.api.db.DepartmentDAO#saveDepartment(org.openmrs.module.department.Department)
         */
        @Override
        public Department saveDepartment(Department department) {
                sessionFactory.getCurrentSession().save(department);
                return department;
        }
        /**
         * @see org.openmrs.module.department.api.db.DepartmentDAO#purgeDepartment(org.openmrs.module.department.Department)
         */
        @Override
        public void purgeDepartment(Department department) {
                sessionFactory.getCurrentSession().delete(department);
        }
}

Service:

/**
 * The service for managing departments.
 */
@Transactional
public interface DepartmentService extends OpenmrsService {
        /**
         * Gets a list of departments.
         *
         * @return the department list.
         */
        @Transactional(readOnly = true)
        List<Department> getAllDepartments();
        /**
         * Gets a department for a given id.
         *
         * @param id the department id
         * @return the department with the given id
         */
        @Transactional(readOnly = true)
        Department getDepartment(Integer departmentId);
        /**
         * Saves a new or existing department.
         *
         * @param department the department to save.
         * @return the saved department.
         */
        Department saveDepartment(Department department);
        /**
         * Deletes a department from the database.
         *
         * @param department the department to delete.
         */
        void purgeDepartment(Department department);
}

ServiceImpl:

/**
 * It is a default implementation of {@link DepartmentService}.
 */
public class DepartmentServiceImpl extends BaseOpenmrsService implements DepartmentService {
        protected final Log log = LogFactory.getLog(this.getClass());
        private DepartmentDAO dao;
        /**
         * @param dao the dao to set
         */
        public void setDao(DepartmentDAO dao) {
                this.dao = dao;
        }
        /**
         * @return the dao
         */
        public DepartmentDAO getDao() {
                return dao;
        }
        /**
         * @see org.openmrs.module.department.api.DepartmentService#getAllDepartments()
         */
        @Override
        public List<Department> getAllDepartments() {
                return dao.getAllDepartments();
        }
        /**
         * @see org.openmrs.module.department.api.DepartmentService#getDepartment(java.lang.Integer)
         */
        @Override
    public Department getDepartment(Integer departmentId) {
            return dao.getDepartment(departmentId);
    }
        /**
         * @see org.openmrs.module.department.api.DepartmentService#saveDepartment(org.openmrs.module.department.Department)
         */
        @Override
        public Department saveDepartment(Department department) {
                return dao.saveDepartment(department);
        }
        /**
         * @see org.openmrs.module.department.api.DepartmentService#purgeDepartment(org.openmrs.module.department.Department)
         */
        @Override
        public void purgeDepartment(Department department) {
                dao.purgeDepartment(department);
        }
}  

Coding conventions and standards

When editing the DAO and service layer classes, don't forget to ensure that your code adheres to our general standards. Refer to the 'Development process' chapter, which will give you detailed instructions on how to ensure this.

Also, don't forget to add Junit Unit tests to validate that the methods you introduced behave exactly as they should. 

Creating the web interface for your module

To make these changes to be accessible to users, you need to make changes to the module controller. You will also need to introduce a new file named addDepartment.jsp into the /omod/src/main/webapp directory. This will contain the jsp page that lets you edit your name. The general contents of this class will be as follows: 

<form method="post">
<fieldset>
<table>
    <tr>
        <td><openmrs:message code="general.name"/></td>
        <td>
            <spring:bind path="department.name">
                <input type="text" name="name" value="${status.value}" size="35" />
                <c:if test="${status.errorMessage != ''}"><span class="error">${status.errorMessage}</span></c:if>
            </spring:bind>
        </td>
    </tr>
    <tr>
        <td valign="top"><openmrs:message code="general.description"/></td>
        <td valign="top">
            <spring:bind path="department.description">
                <textarea name="description" rows="3" cols="40" onkeypress="return forceMaxLength(this, 1024);" >${status.value}</textarea>
                <c:if test="${status.errorMessage != ''}"><span class="error">${status.errorMessage}</span></c:if>
            </spring:bind>
        </td>
    </tr>
</table>
<br />
<input type="submit" value="<openmrs:message code="department.save"/>" name="save">
</fieldset>
</form>

Once the JSP is complete, don't forget to modify the controller to point to this. It is also useful to add validations to asses user input when the controller is triggered.

 @RequestMapping(value = "/module/department/departmentForm.form", method = RequestMethod.POST)
    public String submitDepartment(WebRequest request, HttpSession httpSession, ModelMap model,
                                   @RequestParam(required = false, value = "action") String action,
                                   @ModelAttribute("department") Department department, BindingResult errors) {
        
        MessageSourceService mss = Context.getMessageSourceService();
        DepartmentService departmentService = Context.getService(DepartmentService.class);
        if (!Context.isAuthenticated()) {
            errors.reject("department.auth.required");
        } else if (mss.getMessage("department.purgeDepartment").equals(action)) {
            try {
                departmentService.purgeDepartment(department);
                httpSession.setAttribute(WebConstants.OPENMRS_MSG_ATTR, "department.delete.success");
                return "redirect:departmentList.list";
            }
            catch (Exception ex) {
                httpSession.setAttribute(WebConstants.OPENMRS_ERROR_ATTR, "department.delete.failure");
                log.error("Failed to delete department", ex);
                return "redirect:departmentForm.form?departmentId=" + request.getParameter("departmentId");
            }
        } else {
            departmentService.saveDepartment(department);
            httpSession.setAttribute(WebConstants.OPENMRS_MSG_ATTR, "department.saved");
        }
        return "redirect:departmentList.list";
    }

Now that your module is completed, it is the perfect time to go ahead and test it. First test it yourself to make sure that there are no obvious mistakes before asking a target end user to try it out. The end user's feedback may result in further design discussions or reviews. Once these have been completed, the module can be implemented at the clinic, and also made available publicly. Refer to the guidelines specified in the 'Development Process' chapter to find out the best way to do this.

Once your module is released, you may think that your work is over. However, there is no such thing. As health systems, requirements, and technology change, so must the software.This makes medical informatics a viable career option, but does not mean you are responsible for maintaining Hello World for the rest of its life with OpenMRS.

Sharing your module 

When done with developing and testing your module, you can release it for developers by deploying to the Maven repository using instructions at: http://go.openmrs.org/newdev-tagging

For end users, you can upload it to the module repository http://modules.openmrs.org which is available to everyone. Read more about our rules and regulations for it here: http://go.openmrs.org/newdev-modrepo