2009 Implementers Group Meeting Program Advanced Module Development

Download the code

The module code that was written (and fixed/revised post meeting) can be downloaded: Basicmodule-benscoolnesslevel.zip

Introduction

This tutorial has code examples modified from code created in the previous tutorial in 2009_Implementers_Group_Meeting_Program-How_to_use_the_API.

Module Architecture was introduced in the 2009 Implementers Group Meeting Program-ModuleArchitecture

Follow the instructions at Creating_Your_First_OpenMRS_Module to setup a basic module.

Also check out the 2009_Implementers_Group_Meeting_Program-Development_Best_Practices tutorial notes.

Create a new BensCoolnessLevel object

/**
 * Code for Advanced Module Tutorial by Ben Wolfe at 2009 OpenMRS developers meeting
 * http://openmrs.org/wiki/2009_Implementers_Group_Meeting_Program/Advanced_Module_Development
 */
public class BensCoolnessLevel extends BaseOpenmrsObject {

	/**
     * @see org.openmrs.OpenmrsObject#getId()
     */
    @Override
    public Integer getId() {
	    // TODO Auto-generated method stub
	    return null;
    }

	/**
     * @see org.openmrs.OpenmrsObject#setId(java.lang.Integer)
     */
    @Override
    public void setId(Integer arg0) {
	    // TODO Auto-generated method stub

    }

    private Integer bensCoolnessLevelId = null;
    private Integer level = null;
    	public BensCoolnessLevel(Integer id) {
    	this.bensCoolnessLevelId = id;
    	}

		
        /**
         * @return the bensCoolnessLevelId
         */
        public Integer getBensCoolnessLevelId() {
        	return bensCoolnessLevelId;
        }

		
        /**
         * @param bensCoolnessLevelId the bensCoolnessLevelId to set
         */
        public void setBensCoolnessLevelId(Integer bensCoolnessLevelId) {
        	this.bensCoolnessLevelId = bensCoolnessLevelId;
        }

		
        /**
         * @return the level
         */
        public Integer getLevel() {
        	return level;
        }

		
        /**
         * @param level the level to set
         */
        public void setLevel(Integer level) {
        	this.level = level;
        }

}

If you created your module project using the BaseModule starter and are not able to import BaseOpenmrsObject, check if you have the 1.6 version of the openmrs-api.jar in the lib-common dir.

After extending BaseOpenmrsObject, let Eclipse's code completion the unimplemented methods for the id field.

Use eclipse's shift-Alt SR to generate the getters and setters.

Best Practice: If the object extends BaseOpenmrs object you can take advantage of things like sync. BaseOpenmrs provides id and uuid fields.

Create SQL Diff

Open metadata/sqldiff.xml and add a new diff containing the sql code to add a new table:

<sqldiff version="1.0">

	<diff>
		<version>1.0.0</version>
		<author>Bob Dobalina</author>
		<date>Sep 17, 2009</date>
		<description>
			Adding bens_coolness_level table
		</description>
		<sql>
			CREATE TABLE IF NOT EXISTS `bens_coolness_level` (
			  `id` int(11) NOT NULL auto_increment,
			  `level` int(11) NOT NULL,
			  `uuid` varchar(36) NOT NULL default '',
			  PRIMARY KEY  (`id`)
			) ENGINE=InnoDB DEFAULT CHARSET=utf8;
		</sql>
	</diff>

</sqldiff>

Can use sql ide like Squirrel to create sql syntax for table create = copy/paste into sql diff.

The first time that this module is loaded, this sql will run and the db will be created. Make sure the table does not already exist.

Hibernate mapping

Create a new hibernate mapping file at metadata/BensCoolnessLevel.hbm.xml

<?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="@MODULE_PACKAGE@">

	<class name="BensCoolnessLevel" table="bens_coolness_name">

		<id name="id" type="int" column="id">
			<generator class="native" />
		</id>

		<property name="level" type="java.lang.Integer" length="11"
			column="patient_id" not-null="true" />
		<property name="uuid" type="java.lang.String" column="uuid"
			length="36" unique="true" />

	</class>
</hibernate-mapping>

Tip: If the col name is same as field name, the "name" parameter is not required.

Config file depends on internet connection - delete the schema definition at top (!DOCTYPE hibernate-mapping... that has a public url such as http://hibernate.sourceforge.net...) if you don't have internet access.

Next step - make sure module runs

Overview to Next Steps

Create modifications to moduleApplicationContext.xml and include service Interceptors - transaction interceptor, logging interceptor, save handlers

Upload module in Admin interface

Global Properties and Background on Processing SQL Diffs

Look at the following global properties linked from the Administration page when OpenMRS is running:

  • module id
  • db id is the version of the sql diff that was run. makes it easy to roll back sql diff
  • module,started - If module should start again at next startup
  • mandatory - If module does not start should openmrs start?

If you need to alter tables, create a new sql diff and increment version number. When new user downloads and installs the module, all of the sql diffs are processed. This way users who update the module get the alter table statements - anything new.

Service methods

In previous session Ben created ModuleService

created method SaveBensCoolnessLevel(BenCoolnesslevel level)

Must implement this in the impl

Use the convenience wiz in eclipse to generate the method sig

saveBensCoolnessLevel(BensCoolnessLevel level) {
if (level h1. null) {
throw new APIException
}
}

Create save method in BasicModuleDAO
instead of creating docs, just point to the docs using @see
HibernateBasicModuleDAO - default implementation od DAO - there are defined in applicationContext - find in module wiki page

sessionFactory.getCurrentSession().saveOrUpdate(level);

Implement this method in public void saveBensCoolenssMehtod()Benscoolenssmethod level

Back to BasicModuleService

add a getter

/**
*
* @should 
*/
public BensCoolnessLevel getBensCoolnessLevel(Integer id) {

}

//Then implementation - use eclipse to generate and add the getBensCoolnessLevel

public BensCoolnessLevel getBensCoolnessLevel(Integer id) {

return (BensCoolnessLevel) sessionFacotry.getCurrentSession.get(BensCoolnessLevel.class, id));

}

Best Practice:
If you add a method onto a service, it's a place for another module can hook into. Another way is to use a util method, but other modules cannot hook into it.

Primarily services are used or getters and setters View layer - the jsph1. Typically name the file according to the url that calls it - in this case, BensCoolnessLevel.form

<form method = post action - BensCoolnessLevel.form>
<input type - text value = "${benscoolnesslevel.id}" name = id>
<input type - text value = "${benscoolnesslevel.level}" name = level>
<input type = submit>
</form>

benscoolnesslevel is provided by Spring in the request. Reference in the jsp page as ${benscoolnesslevel.level}. The JSTL fields correspond to the getters in the class.Controllerh1. With annotations you can setup the controller easily. In module applicationContext, add call to controller

<context.component - scan base package - org.openmrs...add.contoller

Then add the controller, which cannot extend any of the Spring controllers

public class BensCoolnessLevelController {

// add the logging
// add the method you need
// ModelMap tells spring what you're giving it.

// annotation - if page loaded for first time, call this page
@RequestMapping(value = "/modules.benscoolnesslevel.benscoolnesslevel", methodRequestMethod  - get
getPageInitially(ModelMap map, HttpsServletRequest request) {

// also can use annotations: getPageInitially(ModelMap map, 

//@requestParam(value = "id" required = false") Integer id {

Stirng id = request.getAttribute("id");
BasicModuleService service - Context.getService(BasicModuleService.clsdd)
BensCoolnessLevel level - sservice.getBensCoolnessLeve(id);
map.addAttribute("BensColneslevel", level);

}
// also create the save method....add the @requestParam for each field you're passing.
}

Then compile.Debugging the moduleh1. Copy important beans to the module's applicationContext.xml magic that tells spring how to load some classes.

The module load failed because component.scan was not defined.

Had to delete the DOCTYPE due to an old doctype - older version of spring.

Failed cause error - id was null - throwing a null id to hibernate causes an error. Need ot tell module about the hibernate file created.
in the config file

Worse practices - don't use unit tests. Otherwise you watch modules loaad and unload, over and over...

Idea: Re-create demo by creating unit tests as we code.Modifications to applicationContext.xmlh1. Include service Interceptors - transaction interceptor, logging interceptor, save handlers

Upload module in Admin interface

So, when you run the app, the db will be created. Make sure the table does not already exist. Global Propertiesh1. Look at global properties -

  • module id,
  • db id is the version of the sql diff that was run. makes it easy to roll back sql diff
  • module,started - if module should start again at next startup
  • mandatory - if module does not start should pennmrs start.

Alter tables - create a new sql diff, increment version number

When new user downloads and installs the module, all of the sql diffs are processed. This way users who update the module get the alter table statements - anything new.Service methodsh1. In previous session Ben created ModuleService

created method SaveBensCoolnessLevel(BenCoolnesslevel level)

Must implement this in the impl

Use the convenience wiz in eclipse to generate the method sig

saveBensCoolnessLevel(BensCoolnessLevel level) {
if (level null) {
throw new APIException
}
}

Create save method in BasicModuleDAO
instead of creating docs, just point to the docs using @see
HibernateBasicModuleDAO - default implementation od DAO - there are defined in applicationContext - find in module wiki page

sessionFactory.getCurrentSession().saveOrUpdate(level);

Implement this method in public void saveBensCoolenssMehtod()Benscoolenssmethod level

Back to BasicModuleService

add a getter

/**
*
* @should 
*/
public BensCoolnessLevel getBensCoolnessLevel(Integer id) {

}

Then implementation - use eclipse to generate and add the getBensCoolnessLevel

public BensCoolnessLevel getBensCoolnessLevel(Integer id) {

return (BensCoolnessLevel) sessionFacotry.getCurrentSession.get(BensCoolnessLevel.class, id));

}

Best Practice: If you add a method onto a service, it's a place for another module can hook into. Another way is to use a util method, but other modules cannot hook into it.

Primarily services are used or getters and sttters

View layer - the jsp

Typically name the file according to the url that calls it - BensCoolnessLevel.form

<form method = post action - BensCoolnessLevel.form>
<input type - text value = "${benscoolnesslevel.id}" name = id>
<input type - text value = "${benscoolnesslevel.level}" name = level>
<input type = submit>
</form>

benscoolnesslevel is provided by Spring in the request. Reference in the jsp page as ${benscoolnesslevel.level}.. The JSTL fields correspond to the getters in the class.

Controller

With annotations you can setup the controller easily. In module applicationContext, add call to controller

<context.component - scan base package - org.openmrs...add.contoller

Then add the controller, which cannot extend any of the Spring controllers

public class BensCoolnessLevelController {

// add the logging
// add the method you need
// ModelMap tells spring what you're giving it.

// annotation - if page loaded for first time, call this page
@RequestMapping(value = "/modules.benscoolnesslevel.benscoolnesslevel", methodRequestMethod  - get
getPageInitially(ModelMap map, HttpsServletRequest request) {
// also can use annotations: getPageInitially(ModelMap map, 
//@requestParam(value = "id" required = false") Integer id {

Stirng id = request.getAttribute("id");
BasicModuleService service - Context.getService(BasicModuleService.clsdd)

BensCoolnessLevel level - sservice.getBensCoolnessLeve(id);

map.addAttribute("BensColneslevel", leve);
}
// also create the save method....add the @requestParam for each field you're passing.
}

Then compile.

Debugging the module

Copy important beans to the module's applicationContext.- xml magic that tells spring how to load some classes.

The module load failed because component.scan was not defined.

had to delete the DCOTYPE due to an old doctpye - older version of spring.

failed cause error - id was null - throwing a null id to hibernate causes an error. Need ot tell module about the hibernate file created.
in the config file

Worse practices - dont' use unit tests. Otherwise you watch modules loaad and unload, over and over...

Idea: Re-create demo by creating unit tests as we code.