OpenMRS Technical Overview


(Redirected from OpenMRS Architecture)

Contents

Enlarge

OpenMRS has been designed to have a tiered architecture. The real strength of OpenMRS is in its robust and flexible data model. However, not all users want to have to know this data model inside and out. The API layer allows a developer to only have to know Java objects and read/save to them. This layer can be used in a standalone application or, as most people use it, in a J2EE web application.

Context

The backbone of OpenMRS is the core API. This API has methods for all of the basic functions like adding/updating a patient, adding/updating a concept, etc. These methods are provided in services. There are classes named PatientService, ConceptService, EncounterService, ObsService, etc. The data model image groups the database tables into "domains." Each domain is a separate colored box. The breakdown of domains/tables is essentially a visual representation of the service separation.

The Context is a static class to allow the application to save on memory. Only one PatientService object, one ConceptService object, etc (and of course the associated DAOs) are instantiated. The Context's services are split into two categories, methods for the Services and for Users. The services are kept in the aptly named ServiceContext class. This is instantiated only once and is stored statically in the Context. The getter methods for the services simply pass through Context to the ServiceContext. The UserContext contains methods for acting on users: authentication and authorization, logging in, logging out, etc. A different UserContext is instantiated for every user accessing into the system. The "current" UserContext is stored on the current thread. When that user is done, the UserContext is taken off of the thread and put into the user's session variable (in the case of the webapp). When the user accesses the system again, the UserContext is taken off of the user's session and placed onto the thread again. In the webapp, this manipulation is done by the OpenmrsFilter class that wraps around every call to the server. Similar to the services, the methods on the Context class pass through to the current UserContext on the current thread.

Every access to the system must be defined within a "unit of work". This unit is bordered by calls to Context.openSession() and Context.closeSession(). In the webapp, these calls are done in OpenmrsFilter and most developers don't have to worry about making those calls. However, any developer of an external application or a thread spinoff (like Hl7InQueueProcessor and FormEntryProcessor) will need to be sure to include open/closeSession calls or risk leaking database connections.

Read more about the OpenMRS API

Spring DI and AOP

PatientService, ConceptService, etc are interfaces. PatientServiceImpl, ConceptServiceImpl, etc are the current default implementations of those interfaces. Which implementation Context serves up is determined by Spring's dependency injection. Each *Impl contains a reference to its DAO (PatientDAO, ConceptDAO, etc). The DAOs are also interfaces. The current default implementation of them are for Hibernate (HibernatePatientDAO, HibernateConceptDAO, etc). Which DAO implementation is used at runtime is also determined by Spring's DI.

All of these implementations are described in the Spring applicationContext-services.xml file. This file also controls the transaction and authorization AOP annotations (See next section).

Each *Service (not *DAO) is considered to be an AOP advice point. Because we instantiate and serve only one *Service class out of the Context, this is easy. When Spring starts, each service that has advice points around it gets wrapped with a Spring AOP class. If there are five modules/methods that wrap around a certain class, there will be five different wrappers. Any one of those could exit method execution early (if it's an "around" advice). Most AOP linking will be done by modules.

Also see OpenMRS AOP.

Authorization and Authentication

OpenMRS has a very granulated permissions system. Every action is associated with a Privilege. An action would be "Add Patient", "Update Patient", "Delete Patient", "Add Concept", "Update Concept", etc. A Role contains a collection of Privileges. A Role can also point to a list of inherited roles. The role inherits all privileges from that inherited role. In this way, hierarchies of roles are possible. A User contains only a collection of Roles, not Privileges.

AOP annotations are used to require a privilege for a service method. The @Authorized({"Add Patients"}) annotation is placed in the interface of the service. If the current user does not possess that privilege an APIAuthenticationException is thrown. (The webapp catches this exception and redirects to the login form).

Each page in the webapp is able to require a certain privilege for the page with the openmrs:require taglib. The openmrs:hasPrivilege taglib provides support for restricting only certain sections of a page.

While using the API, you may come to a point where you need to temporarily grant the current user a certain privilege in order to make an API call. This is accomplished using the Context.addProxyPrivilege(String priv) and removeProxyPrivilege(String priv) methods. Multiple priv objects defining the same string can be proxied. Subsequent calls to removePrivilege will only pop the first one off the stack. Best practice says that you should put your API method calls in a try/catch block and put the removeProxyPrivilege call in a finally block.

Hibernate

Hibernate is an excellent Object Relational Mapper. Using just xml files, we are able to describe the relationship between all of our tables and our domain (POJO) objects (like Patient.java, Concept.java, etc). Looking at the concept domain in the datamodel, we see that it consists of tables named concept, concept_answer, concept_set, concept_name. It would be very difficult to keep up with where to store each part of the concept object and the relations between them. Using Hibernate, we only need to concern ourselves with the Concept object, not the tables behind the object. The concept.hbm.xml mapping file does the hard work of knowing that the Concept object contains a collection of ConceptSet objects, a collection of ConceptName objects, etc. To add a new name to a concept:

ConceptService conceptService = Context.getConceptService();
Concept concept = conceptService.getConcept(1234);
ConceptName newConceptName = new ConceptName("some name", "some locale");
concept.addName(newConceptName);
conceptService.updateConcept(concept);

Hibernate knows what has changed and what needs to be saved into the database. (The long and short of it is that Hibernate wraps the Concept object in its own object and keeps track of what has been added, removed, etc).

Hibernate will not load all associated objects until they are needed -- this is called lazy loading. The concept object above never dove into the concept_answer table to get the list of answers for concept 1234. If we had called concept.getConceptAnswers() Hibernate at that point would have made a call to the database to retrieve the answers for us. For this reason, you must either fetch/save/manipulate your object in the same session (between one open/closeSession) or you must hydrate all object collections in the object by calling the getters (getConceptAnswers, getConceptNames, getSynonyms, etc).

Most Useful ANT Build.xml Targets

clean 
Deletes the dist and build directories. Our custom build file overwrites certain @____@ strings in the code as described in the init method. If your IDE has compiled this file for you, the ANT file won't compile it and won't, therefore, overwrite those strings. Using the ANT clean target will delete those compiled files and a quick call to another ANT target that compiles code will be sure to clean those up.
compile-api 
Usually just called by other targets. Compiles the api code into the /build folder
package-api 
packages the api code up into a jar file. The jar file is placed in the /dist folder. This target will call compile-api.
package-javadoc-api 
Compiles and packages the javadoc for the api. Places a javadoc.zip file into the dist folder.
package-api-tests 
Compiles and packages the OpenMRS tests in /test into a tests-openmrs-api.jar file
deploy-web 
very useful. Copies all .jsp, .js, .properties, .css, and images to the webapps folder in the defined tomcat home directory. When working solely in the webapp environment, this target provides rapid changing and testing of jsp and javascript. Assumes there is a war file deployed already.
package-web 
Creates an openmrs.war file in the dist directory
package-web-without-libs 
Creates an openmrs.war file in the dist directory that does not have the include all of the jar files in the /lib folder. This creates a much smaller war file. (Assumes the jars will be distributed separately. See next target).
package-web-libs-only 
Creates a zip file of just the jars in the /lib folder.
dist 
synonym for the package-web target. Creates an openmrs.war file in the /dist directory.
compile-api-tests
MUST be run before running any JUnit tests. Copies the right mapping and xml descriptor files to the folders that the tests assume have them.
install 
Packages the war file and installs it into the tomcat defined by the tomcat.manager.url property. Assumes there is not a war file deployed named openmrs already.
remove 
Removes (undeploys) the war file from tomcat
update 
Essentially removes and then installs the openmrs war in tomcat.
reload 
Overwrites the files in the tomcat webapps directory with the contents of openmrs.war. Does not work reliably in all situations. Update is preferred (but reload is faster)
start 
Start the webapp in tomcat. Assumes the war has been deployed (installed) and is stopped already.
stop 
Stops the webapp in tomcat. Assumes the war has been deployed (installed) and is started already.
tomcat-start 
Starts the tomcat process and runs standard application output through the console in your IDE.
tomcat-stop 
Stops the tomcat process.

Modules

Module documentation for developers.

Spring MVC

OpenMRS strongly subscribes to the Model-View-Controller pattern. We won't go into the depths of MVC, or even the basics of it. Mediawiki has a complete writeup about everything you may have wanted to know about the MVC programming pattern. Spring has also written a fair amount on how and why to use MVC. OpenMRS, for the most part, uses the domain objects as the model. Most controllers will be SimpleFormControllers and be placed in the org.openmrs.web.controller package. The model is set up in the controller's formBackingObject, and processed/saved in the processFormSubmission and onSubmit methods. The jsp views are placed in /web/WEB-INF/view.

Not all files served by the webapp are run through Spring. The /web/WEB-INF/web.xml file maps certain web page extensions to the SpringController. All *.form, *.htm, and *.list pages are mapped. The SpringController then uses the mappings in the openmrs-servlet.xml file to know which pages are mapping to which Controller.

There are no .jsp pages that are accessed directly. If a pages url is /admin/patients/index.htm, the jsp will actually reside in /web/WEB-INF/view/admin/patients/index.jsp. This is necessary so that we can do the redirect with the SpringController. Because the file being accessed ends with .htm, the SpringController is invoked by the web server. When the SpringController sees the url, it simply replaces .htm with .jsp and looks for the file in /web/WEB-INF/view/ according to the jspViewResolver bean in openmrs-servlet.xml. If the page being accessed was patient.form, the mapping in the urlMapping bean would have told spring to use the PatientFormController and the patientForm.jsp file.

That spring descriptor file also contains settings for the max form upload size, locale changing, message names, report filters (should be (re)moved), fieldGen handlers, and name/address templates.

DWR

Direct Web Remoting is a framework that allows us to translate java objects and methods to javascript objects and methods. Together with DOJO, DWR forms the basis of the AJAX in OpenMRS. The dwr.xml descriptor file describes which classes and methods are translated and made available to javascript calls. Most base DWR-able classes are placed into the org.openmrs.web.dwr package.

DOJO

DOJO is a javascript library and packaging system. All dojo packages are stored in /web/WEB-INF/view/scripts/dojo. The custom OpenMRS javascript "classes" are stored in /web/WEB-INF/view/scripts/dojo/src/widget/openmrs.