OpenMRS 2.0 UI Framework Options

advertisement



As we rewrite the UI, we have a one-time
opportunity to pick a new web framework
All options are on the table
Criteria






Plug in our existing Java API
Support extensions via modules
Allow us to build reusable components
Easy! Fun! Beautiful!
Accessible to entry-level coders
Well-supported

Spring 3




Ruby on Rails (via JRuby)


JQuery
Grails



Spring MVC + JQuery + roll-our-own widgets
Spring MVC + JQuery + StringTemplate
JSF + ICEFaces
YUI
JQuery
GXT


“the leading platform to build and run enterprise
Java applications. Led and sustained by
SpringSource, Spring delivers significant benefits
for many projects, increasing development
productivity and runtime performance while
improving test coverage and application quality”
Why?
◦ We use it, we like it
◦ Extremely flexible
◦ Heavily used, and getting better all the time






http://svn.openmrs.org/openmrs-contrib/uiframeworks/spring3mvc+jquery
◦ Thanks Harsha!
The modern version of what we're doing now
Maven! Jetty!
Spring 3 MVC has lots of timesaving features
we haven’t taken advantage of
Jquery is powerful and we haven't taken
advantage
Consciously build reusable UI widgets
@Controller
public class PatientController {
@RequestMapping(value="/patient", method=RequestMethod.GET)
public String viewPatient(@RequestParam("patientId") Patient patient,
Model model) {
model.addAttribute(patient);
if (patient != null)
model.addAttribute("encounters", Context.getEncounterService().getEncountersByPatient(patient));
}
}
<script type="text/javascript">
$(function() {
$('#tabs').tabs();
});
</script>
...
<div id="tabs">
<ul>
<li><a href="#tabs-1">Patient</a></li>
<li><a href="#tabs-2">Encounters</a></li>
</ul>
...
<div id="tabs-2">
<h1>Patient Encounters</h1>
<openmrs:encounters
encounterList="${encounters}"/>
</div>
<%@ attribute name="encounterList" required="true” type="java.util.List" %>
...
<script type="text/javascript">
$(document).ready(function() {
$('#encounters').dataTable();
});
</script>
<table id="encounters" border="1" width="500" cellspacing="0">
<thead>
<tr>
<th>Encounter Id</th>
<th>Encounter Date</th>
<th>Encounter Patient</th>
</tr>
</thead>
<tbody>
<c:forEach var="encounter" begin="0" items="${encounterList}">
<tr class="gradeX">
<td>${encounter.encounterId}</td>
<td>${encounter.encounterDatetime}</td>
<td>${encounter.patientId}</td>
</tr>
</c:forEach>
</tbody>
</table>

Pros:
◦
◦
◦
◦

Flexibility (never “not be able to do” something)
Strong community
Configured right, it allows RAD
No learning curve for existing devs
Cons:
◦ Significant learning curve for newbies (lots of
technologies, and they are not simplified)

Open questions:
◦ Can we leverage Hibernate annotations?

http://github.com/diptanu/openmrs-beta/tree/module
◦ -Thanks Thoughtworks!





“[StringTemplate's] distinguishing characteristic is that it
strictly enforces model-view separation unlike other engines.
Strict separation makes websites and code generators more
flexible and maintainable.”
Controllers like you're used
StringTemplate instead of JSP
Recommended by Thoughtworks developers
Templates allow for page layouts and
reusable components
layout/layout.st
<html>
<head>
widgets/encounters.st
<title>Patient Dashboard</title>
$if(encounters)$
<link rel="stylesheet" href="../../../styles/jquery.ui.all.css" type="text/css">
<script type="text/javascript">
<script type="text/javascript" src="../../../scripts/jquery-1.4.2.js"></script>
jQuery(function() {
<script type="text/javascript" src="../../../scripts/jquery-ui-1.8.2.custom.min.js"></script>
jQuery('#encounters').dataTable();
<script type="text/javascript" src="../../../scripts/jquery.dataTables.js"></script>
});
</head>
</script>
<table id="encounters" border="1" width="500" cellspacing="0">
<body>
<thead>
<div class="header">$partials/header()$</div>
<tr>
<div class="content">$body$</div>
<th>Date</th>
<div class="footer">$partials/footer()$</div>
<th>Location</th>
</body>
<th>Type</th>
</html>
<th>Provider</th>
</tr>
</thead>
<tbody>
patientdashboard.st
$encounters:{ encounter |
<div id="tabs">
<tr class="gradeX">
<ul>
<td>$encounter.encounterDatetime$</td>
<li><a href="#tabs-1">Patient</a></li>
<td>$encounter.location.name$</td>
<li><a href="#tabs-2">Encounters</a></li>
<td>$encounter.encounterType.name$</td>
$menuext:{item|
<td>$encounter.provider.givenName$</td>
<li><a href="#tabs-3">$item$</a></li>
</tr>
}$
}$
</ul>
</tbody>
...
</table>
<div id="tabs-2">
$widgets/encounters(encounters=patient.encounters)$
$else$
</div>
<b>There are currently no encounters</b>
...
$endif$

Pros:
◦ Strict Model-View separation will make us program
better

Cons:
◦ Unclear whether this approach has significant
advantages over more-commonly-used technologies

Open Questions:
◦ When a module attaches to an extension point, that now
becomes view-only, and can’t add any data to the
model. Right?



http://svn.openmrs.org/openmrs-contrib/ui-frameworks/jsf+icefaces/
◦ Thanks Shazin! (ICEFaces 1.8, JSF 1.2, no Spring)
◦ JSF 2 with Spring in progress
JSF: Developed through the Java Community Process under JSR - 314, JavaServer Faces
technology establishes the standard for building server-side user interfaces. With the
contributions of the expert group, the JavaServer Faces APIs are being designed so that they
can be leveraged by tools that will make web application development even easier
ICEFaces: J2EE Ajax framework for developing and deploying rich enterprise applications
(REAs). With ICEfaces, enterprise Java developers can easily develop rich enterprise applications
in Java, not JavaScript

JSF instead of Spring MVC

Facelets instead of JSP

No Javascript required! JSF automatically reloads page
fragments as required
◦ Component-based
◦ Server-side state, stored in the session
// JSF 1.2 with XML-defined beans
public class TableBean implements PageBean {
...
private List headers;
<ice:form>
private List<EncounterDTO> body;
<ice:hiddenText binding="#{patient.init}" />
<ice:panelTabSet>
public List<EncounterDTO> getBody() {
...
return body;
<ice:panelTab id="encounters" label="#{msgs['encounters.tab.label']}">
}
<ice:panelGrid id="panelGrid2" >
...
public void setBody(List<EncounterDTO> body) {
<ice:dataTable value="#{table.body}" var="item">
this.body = body;
<ice:column>
}
<f:facet name="header">
<ice:outputText value="#{msgs['encounters.date.label']}"/>
public List getHeaders() {
</f:facet>
return headers;
<ice:outputText value="#{item.date}"/>
}
</ice:column>
<ice:column>
public void setHeaders(List headers) {
<f:facet name="header">
this.headers = headers;
<ice:outputText value="#{msgs['encounters.location.label']}"/>
}
</f:facet>
}
<ice:outputText value="#{item.location}"/>
</ice:column>
// JSF 2.0 with Spring-managed beans
...
</ice:dataTable>
@Component("helloWorld")
</ice:panelGrid>
@Scope("session")
</ice:panelTab>
public class HelloWorldBean {
</ice:panelTabSet>
</ice:form>
public String getMessage() {
...
return "Hello World!";
}
public User getAuthenticatedUser() {
return Context.getAuthenticatedUser();
}
}

Pros:
◦ JSF is the J2EE 6 Standard
◦ ICEFaces looks good

Cons:
◦ Steep learning curve
◦ Poor RAD (editing a single server-side component
requires restarting Jetty)

Open Questions:
◦ How will module extensions work in a componentbased framework?




http://svn.openmrs.org/openmrs-contrib/uiframeworks/jruby+rails+jquery/
“Ruby on Rails is an open-source web framework
that’s optimized for programmer happiness and
sustainable productivity. It lets you write
beautiful code by favoring convention over
configuration”
“A domain-specific-language for databasebacked web applications”
Thoughtworks CTO recommends Java backends
with RoR-via-JRuby front-ends
Conventions for project layout
•app
•controllers
•helpers
•models
•views
•layouts
•(controller)
•(action)
•config
•environments
•initializers
•locales
•public
•images
•javascripts
•stylesheets
class PatientController < ApplicationController
def view
@patient = Context.patientService.getPatient(params[:id].to_i)
if @patient.nil?
puts "No patient found!"
raise "No patient found!"
end
@encounters = Context.encounterService.getEncountersByPatient(@patient)
# record that this user viewed this patient
PatientViewed.record_view($omrs.authenticated_user, @patient)
end
def find
ret = [];
$omrs.patient_service.getPatients(params[:query]).each do |patient|
ret << helpers.convert_to_dto(patient)
end
render :json => ret
end
end
<script type="text/javascript">
$(document).ready(function() {
$("#tabs").tabs();
});
</script>
<h1><%= format @patient %> - <%= format @patient.patient_identifier %></h1>
…
<div id="tabs-2">
<%= render :partial => "widgets/encounter_list",
:locals => { :encounters => @encounters } %>
</div>


ActiveRecord and Migrations make it very easy to create
(application-level) database-backed functionality
jruby script/generate model PatientViewed user_id:int patient_id:int
The “domain object” doesn’t declare any properties, because those
are implied by the database table
This migration is automatically created
class CreatePatientVieweds < ActiveRecord::Migration
def self.up
create_table :patient_vieweds do |t|
t.integer :user_id
t.integer :patient_id
t.timestamps
end
end
def self.down
drop_table :patient_vieweds
end
end
class PatientViewed < ActiveRecord::Base
# get recent PatientVieweds for the given user
def self.recently_viewed(user_id, n=5)
all(:conditions => ["user_id = ?", user_id], :order => "created_at
DESC", :limit => n )
end
# get ids of patients recently viewed by the current user
def self.recently_viewed_ids(user_id, n=5)
recently_viewed(user_id, n).collect { |pv| pv.patient_id }
end
# record that a User viewed a Patient
def self.record_view(openmrsuser, openmrspatient)
destroy_all({ :user_id => openmrsuser.user_id, :patient_id =>
openmrspatient.patient_id })
create({ :user_id => openmrsuser.user_id, :patient_id =>
openmrspatient.patient_id })
end
end

Pros:
◦ It’s Fun!
◦ Excellent RAD



Mongrel + Rails seems to be better than Jetty +Spring for this
ActiveRecord and migrations
Cons:
◦ Rails session != Java session => need to write our own session management
◦ No idea of Services


Domain objects are supposed to be intelligent
Wouldn’t be able to use the full power of the framework, because of our dumb domain
objects.
◦ Potential for some annoying incompatibility to hit us out of the blue

Open Questions:
◦ How would module extensions work? Would an omod contain Java code *and* Ruby
code? (Yuck.)
◦ How would Ruby migrations work with Liquibase?
◦ How good is Eclipse integration?
From views/widgets/_patient_search.html.erb
...
<script type="text/javascript">
var options = {
clickUrl: function(rowNum, item) {
return "/patient/view/" + item.patient_id;
},
icon: function(rowNum, item) {
return '<img src="/images/' + (item.gender == 'M' ? 'male' : 'female') + '.png"/>';
},
…
};
$(document).ready(function() {
$('#<%= id %>_form').submit(function() {
$.getJSON("<%= patient_search_opts.search_url %>",
{ query: $('#<%= id %>_query').val() },
<%= id %>_results_results_callback(options)
);
return false;
})
});
</script>
<form id="<%= id %>_form">
ID or Name: <input id="<%= id %>_query" type="text"/> <input type="submit" id="<%= id %>_button"/>
</form>
<%= render :partial => "widgets/vertical_panel",
:locals => { :id => "#{id}_results", :options => patient_search_opts } %>


“GRAILS: the search is over…
Have your next Web 2.0 project done in weeks instead of
months. Grails delivers a new age of Java web application
productivity.”
Attempts to replicate the magic of RoR in the
Java world (used to be called “Groovy on
Rails” until RoR asked them to stop)
◦ Uses Spring, Hibernate, SiteMesh, Jetty, …

Groovy
◦ Java + dynamic typing + closures + compiler magic
◦ 99.9% of legal Java is legal Groovy
def results = Context.patientService.getPatients(“darius”).collect {
[ ptId: it.patientId,
name: it.personName.toString() ]
}

http://svn.openmrs.org/openmrs-contrib/ui-frameworks/grails-poc-dj/
class PatientController {
def view = {
def p = Context.patientService.getPatient(params.int['patientId']);
[ patient: p, encounters: Context.encounterService.getEncountersByPatient(p) ]
}
def searchJson = {
List<Patient> pats = Context.getPatientService().getPatients(params['query']);
def output = pats.collect {
[ patientId: it.patientId,
age: it.age,
gender: it.gender,
name: it.personName.toString() ]
}
render output as JSON
}
}
class OpenmrsTagLib {
static namespace = 'openmrs'
def format = { attrs ->
def obj = attrs['object']
f (obj != null) {
if (obj instanceof User) {
out << "${obj.username} (${formatName(obj.person)})“
} else {
out << "${obj} (don't know how to handle ${obj.class})"
}
}
String ajaxSearchHelper(Map attrs) {
…
return """
<form id="${id}_form“>
<input id="${id}" type="text" size="40"/>
<input type="submit" id="${id}_button" value="Search"/>
</form>
<div id="${id}_results" class="vertical-panel"></div>
<script type="text/javascript“>
${createDecoratorIfNecessary}
\$(document).ready(function() { \$("#${id}_form").submit(function() {
…
"""
}

http://svn.openmrs.org/openmrs-contrib/ui-frameworks/grails-poc/
◦ Thanks Harsha!
<gui:tabView id="tabView">
…
<gui:tab id="t1" label="Encounters">
<h2>Patient Encounters</h2>
<gui:dataTable
draggableColumns="true“
columnDefs="[
[date:'Date', sortable:true, resizeable: true],
[location:'Location', sortable:true, resizeable: true],
[type:'Type', sortable:true, resizeable: true],
[provider:'Provider', sortable:true, resizeable: true]]“
sortedBy='date‘
controller="patient”
action="patientEncounters“
params="[id:'${model.id}']"
caption="click on a row, and it will expand"
paginate="true"
rowExpansion="false"
rowsPerPage="20"
totalRecordsKey="meTotalRecs"
/>
</gui:tab>
<openmrs:extensionPoint
pointId="org.openmrs.patientDashboardTab" type="html">
…
</openmrs:extensionPoint>
…
</gui:tabView>
<head>
<meta name="layout" content="openmrs"/>
</head>
<body>
<h1>Find a patient</h1>
<g:form controller=“patient” action=“find">
ID or Name:
<g:textField name="id”/>
<g:submitButton name="submit" value=“Search"/>
</g:form>
…
<body>
It’s unlikely that we’d choose
YUI over Jquery, even if it
does come in the Grails-UI
plugin…

Pros:
◦ It’s Fun!
◦ Almost no learning curve for current developers
◦ RAD almost as good as in Rails


GORM (analog of ActiveRecord)
Jetty (needs to restart more often than Mongrel)
◦ Sitemesh handles page templates
◦ Auto-recompiling groovy taglibs are great for pulling repeated code out of pages

Cons:

Open Questions:
◦ Small community, possible lack of future support
◦ IDE integration is surprisingly mediocre
◦ A couple times I’ve read: “we lost almost all the productivity gains Grails gave us
because of undocumented bugs in Grails”. (Personally Google searching has found
me answers for all errors I’ve searched for.)
◦ Can we use the embedded database to allow you to develop without even having a
MySQL database?
◦ Is Spring Roo worth looking into as an alternative?






http://svn.openmrs.org/openmrs-contrib/ui-frameworks/spring3+gxt/
-Thanks Sy!
GWT is a development toolkit for building and optimizing
complex browser-based applications
Ext JS is a cross-browser JavaScript library for building rich
internet applications. Ext JS features high performance,
customizable UI widgets and a well-designed and extensible
component model.
=> Ext GWT is a Java library for building rich internet
applications with Google Web Toolkit (GWT)
You only write Java! GWT produces HTML, Javascript, AJAX, …
“Rich Internet Application” instead of “a webapp”
Client UI code looks like a Swing app
public class MainContent extends LayoutContainer {
…
public void openPatientDashboard(PatientDTO patient) {
removeAll();
add(new PatientDashboard(patient));
layout();
}
}
public class PatientDashboard extends LayoutContainer {
…
@Override
protected void onRender(Element parent, int pos) {
super.onRender(parent, pos);
setLayout(new RowLayout(Orientation.VERTICAL));
…
TabItem demographics = new TabItem("Demographics");
demographics.add(createDemographicsForm());
TabItem encounters = new TabItem("Encounters");
encounters.add(new PatientEncounterPanel2(patient));
tabs = new TabPanel();
tabs.add(demographics);
tabs.add(encounters);
…
}
You need lots of (formulaic) classes:
(client)
interface PatientService
interface PatientServiceAsync
(common)
class PatientDTO
(server)
class PatientServiceImpl
DTOs, for sending data between client and server
package web.openmrs.common;
…
public class EncounterDTO implements Serializable {
private Integer id;
private Date date;
private String location;
private String type;
private String provider;
private List<ObsDTO> obs;
…
}

This would be a completely different sort of
application

Pros:

Cons:

Open Questions:
◦ No HTML or Javascript!
◦ “Since this is Java, the API and JavaDocs are available in the IDE as context
sensitive help and the source code is available to look at instead of
referring to (outdated) webpages, also has a large library of examples and
showcase”
◦ Have to build a set of DTOs and services specifically for GWT
◦ Steep(?) learning curve for existing and newbie devs
◦ “the initial load to the client's browser is fairly large but a good framework
for loading is in place. the plus side to this is once it is loaded, the page
never has to be loaded again so page loads do not exist, only data calls
(reducing input/output over the wire as well as server load)”
◦ Could modules plug in, given the client-server architecture?
Spring3-JSP
Spring3StringTemplate
Spring3-JSFICEFaces
JRuby-Rails
Grails
GXT
Learning
curve (new
dev)
Medium
Medium
High
Low
Low
High?
Learning
curve
(existing dev)
None
Low
High
Low
Trivial
High?
OpenMRS
Modules
Easy
???
???
???
Easy
???
Embedded
webserver
Good (Jetty)
Good (Jetty)
OK (Jetty)
Excellent
(Mongrel)
Good (Jetty)
Good
Embedded DB
in dev mode
???
???
???
???
???
???
RAD domain
objects
??? (Hibernate
annotations?)
??? (Hibernate
annotations?)
???
Yes
(ActiveRecord)
Yes (GORM)
No?
Layouts
Need to
integrate
something
Yes
N/A?
Yes
Yes (Sitemesh)
N/A
Built-in
Widgets
None
None
ICEFaces
Won’t use
Won’t use
EXT
Tech for
widgets
Roll our own
StringTemplate
JSF
Partials,
helpers
Taglibs,
templates
“tight and
extensible”
Eclipse
integration
Good
Good
???
??? (RadRails)
Mediocre
(Spring
plugin)
Excellent
(Google
plugin)
Community
Large
Large*
Large
Large*
Small
Small
Concerns
No Services
Modules may
be impossible


More review of JSF 2 + ICEFaces 2
Is GXT even an option?
◦ Can it support modules?



Can we use an embedded DB in development
mode with any of these technologies?
Can Hibernate Annotations provide some of
the functionality of ActiveRecord/GORM in
Spring-3-based technologies?
JSR-286 Portlets
Download