Very cool… of course, it’s all Chinese too me. Here’s Google’s translation back to English:
Thanks to Yang & team, Harsha, and all who contributed!
I’ve long been bothered by how long it takes for our demo site (http://demo.openmrs.org) to be reset. The site goes down for a few minutes each hour as the database is restored and the application restarted. I was working on the demo site today in the middle of one of these resets and so I decided to look around for a better solution.
Fortunately, Akshay Suryawanshi from Percona (percona.com) recently shared a nifty rename_db script that can rename a MySQL database in a couple of seconds. So, we put our credentials and host information into a file (don’t forget chmod 600 .my.cnf to protect those credentials):
[client]
user=openmrs
password=secret
host=127.0.0.1
port=3306
Assuming the credentials and other settings are correct, then this script should show existing databases:
#!/bin/bash
mysql --defaults-extra-file=.my.cnf -e "show databases"
Then we take Akshay’s rename_db script, define DEFAULTS=.my.cnf at the beginning, replace the “-h $1” references with --defaults-extra-file=$DEFAULTS, and remove the initial host parameter:
Adapted from Akshay Suryawanshi’s blog post, thanks to the generous folks at Percona for sharing.
#!/bin/bash
# Copyright 2013 Percona LLC and/or its affiliates
# http://www.mysqlperformanceblog.com/2013/12/24/renaming-database-schema-mysql/
#
# Changes
# - Uses --defaults-extra-file for credentials, host, and port settings
set -e
DEFAULTS=.my.cnf
if [ -z "$2" ]; then
echo "rename_db "
echo "(assumes $DEFAULTS defines user, password, host, and port under [client] section)"
exit 1
fi
db_exists=`mysql --defaults-extra-file=$DEFAULTS -e "show databases like '$2'" -sss`
if [ -n "$db_exists" ]; then
echo "ERROR: New database already exists $2"
exit 1
fi
TIMESTAMP=`date +%s`
character_set=`mysql --defaults-extra-file=$DEFAULTS -e "show create database $1\G" -sss | grep ^Create | awk -F'CHARACTER SET ' '{print $2}' | awk '{print $1}'`
TABLES=`mysql --defaults-extra-file=$DEFAULTS -e "select TABLE_NAME from information_schema.tables where table_schema='$1' and TABLE_TYPE='BASE TABLE'" -sss`
STATUS=$?
if [ "$STATUS" != 0 ] || [ -z "$TABLES" ]; then
echo "Error retrieving tables from $1"
exit 1
fi
echo "create database $2 DEFAULT CHARACTER SET $character_set"
mysql --defaults-extra-file=$DEFAULTS -e "create database $2 DEFAULT CHARACTER SET $character_set"
TRIGGERS=`mysql --defaults-extra-file=$DEFAULTS $1 -e "show triggers\G" | grep Trigger: | awk '{print $2}'`
VIEWS=`mysql --defaults-extra-file=$DEFAULTS -e "select TABLE_NAME from information_schema.tables where table_schema='$1' and TABLE_TYPE='VIEW'" -sss`
if [ -n "$VIEWS" ]; then
mysqldump --defaults-extra-file=$DEFAULTS $1 $VIEWS > /tmp/${1}_views${TIMESTAMP}.dump
fi
mysqldump --defaults-extra-file=$DEFAULTS $1 -d -t -R -E > /tmp/${1}_triggers${TIMESTAMP}.dump
for TRIGGER in $TRIGGERS; do
echo "drop trigger $TRIGGER"
mysql --defaults-extra-file=$DEFAULTS $1 -e "drop trigger $TRIGGER"
done
for TABLE in $TABLES; do
echo "rename table $1.$TABLE to $2.$TABLE"
mysql --defaults-extra-file=$DEFAULTS $1 -e "SET FOREIGN_KEY_CHECKS=0; rename table $1.$TABLE to $2.$TABLE"
done
if [ -n "$VIEWS" ]; then
echo "loading views"
mysql --defaults-extra-file=$DEFAULTS $2 < /tmp/${1}_views${TIMESTAMP}.dump
fi
echo "loading triggers, routines and events"
mysql --defaults-extra-file=$DEFAULTS $2 < /tmp/${1}_triggers${TIMESTAMP}.dump
TABLES=`mysql --defaults-extra-file=$DEFAULTS -e "select TABLE_NAME from information_schema.tables where table_schema='$1' and TABLE_TYPE='BASE TABLE'" -sss`
if [ -z "$TABLES" ]; then
echo "Dropping database $1"
mysql --defaults-extra-file=$DEFAULTS $1 -e "drop database $1"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.columns_priv where db='$1'" -sss` -gt 0 ]; then
COLUMNS_PRIV=" UPDATE mysql.columns_priv set db='$2' WHERE db='$1';"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.procs_priv where db='$1'" -sss` -gt 0 ]; then
PROCS_PRIV=" UPDATE mysql.procs_priv set db='$2' WHERE db='$1';"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.tables_priv where db='$1'" -sss` -gt 0 ]; then
TABLES_PRIV=" UPDATE mysql.tables_priv set db='$2' WHERE db='$1';"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.db where db='$1'" -sss` -gt 0 ]; then
DB_PRIV=" UPDATE mysql.db set db='$2' WHERE db='$1';"
fi
if [ -n "$COLUMNS_PRIV" ] || [ -n "$PROCS_PRIV" ] || [ -n "$TABLES_PRIV" ] || [ -n "$DB_PRIV" ]; then
echo "IF YOU WANT TO RENAME the GRANTS YOU NEED TO RUN ALL OUTPUT BELOW:"
if [ -n "$COLUMNS_PRIV" ]; then echo "$COLUMNS_PRIV"; fi
if [ -n "$PROCS_PRIV" ]; then echo "$PROCS_PRIV"; fi
if [ -n "$TABLES_PRIV" ]; then echo "$TABLES_PRIV"; fi
if [ -n "$DB_PRIV" ]; then echo "$DB_PRIV"; fi
echo " flush privileges;"
fi
We use the following script to backup the OpenMRS database:
#!/bin/bash
mysqldump --defaults-extra-file=.my.cnf --add-drop-database --extended-insert \
--single-transaction openmrs > openmrs.sql
Then we create the following scripts:
#!/bin/bash
mysql --defaults-extra-file=.my.cnf -e "drop database openmrs"
#!/bin/bash
mysql --defaults-extra-file=.my.cnf \
-e "create database openmrs1 DEFAULT CHARACTER SET utf8"
mysql --defaults-extra-file=.my.cnf openmrs1 < openmrs.sql
#!/bin/bash
./drop-openmrs
./rename_db openmrs1 openmrs
Now, when we are preparing to reset the openmrs database, we can execute the restore-openmrs1 script, which will place a fresh copy of the default OpenMRS data into the openmrs1 database (restoring the database can take several seconds, but since we are doing this into openmrs1 and the demo is using openmrs, we can perform this step before involving the demo site). Then, when we are ready to reset the database, simply executing replace-openmrs will reset the openmrs database in 2-3 seconds or less. Given that the demo data is a non-production system, we could even perform this data replacement without restarting the demo application.
Now, instead of taking 30-90 seconds or more, the OpenMRS Demo can be reset in 2-3 seconds or less.
Thank you Akshay Suryawanshi and the folks at Percona for sharing!
I’m very excited about the OpenMRS SDK and what it can mean for the OpenMRS Developer Community.
Chris Niesel (h3llborn) brought this beautiful creature to life during Google Summer of Code 2013 under the mentorship of Rafał Korytkowski. Strong work, guys! Looking forward to watching the SDK grow into both the first thing a new OpenMRS Developer touches and an invaluable tool for daily development of our most experienced developers!
If you own a Mac and haven’t discovered Alfred 2 yet, well I’m sorry. Combined with its PowerPack, it’s a formidable tool for doing things quickly on the Mac. I’ve used QuickSilver and LaunchBar. I held out for a long time as a staunch LaunchBar user, but recently made the switch to Alfred 2 and I’ve got it doing anything everything I need (and boy do I need a lot… I’m lazy and I’ll love keyboard shortcuts). The only shortcoming with Alfred is its clipboard history, which is limited to text (no images… bummer). LaunchBar does the clipboard history just right, so I map Alt+Cmd+K to LaunchBar’s clipboard history and I’m good to go. Sure, LaunchBar is an expensive tool just for clipboard history, but I already owned it and why use Alfred’s crappy clipboard history when I have access to LaunchBar? 🙂
Anyway, I thought I’d share a few of my (now ~24) Alfred 2 workflows for anyone interested:
| Arrows Easily type arrow characters (↑, ↓, ←, →) using HTML entity names: uarr, darr, larr, and rarr. |
|
| IRC Cloud Open IRC Cloud in Chrome with irc. If you’ve already got IRC Cloud in a tab, it doesn’t open it in a second tab. |
|
| OpenMRS A workflow for OpenMRS developers with a few handy shortcuts.
|
|
| Volume Controls Provides some shortcuts for controlling your volume. You thought you were lazy? I know there are dedicated keys for this, but they’re way over there. Alfred shortcuts can be habituated (once learned, you just think and it happens) and don’t require your hands to leave the “home row” of the keyboard.
|
OpenMRS has some basic conventions for its repositories in GitHub (within the openmrs org). Basically, we add four teams to every repo (owners, full committers, partial committers, and repo owners) and we disable wiki & issues on the repo (since we already have a place for wiki & issues).
It’s easy for these things to get overlooked as new repos are added to the org in GitHub, so I made a little Groovy script to manually audit the repos. Nothing fancy and it doesn’t really warrant a repo of its own, but I want to get the code off my laptop… so I’m blogging it. 🙂
The output should look something like this:
$ groovy AuditOpenMRSRepos.groovy
OpenMRS org has 78 repos
Owners team has 78 repos, missing none
Full Committers team has 78 repos, missing none
Partial Committers team has 77 repos, missing none
Repo Owners team has 77 repos, missing none
wikis or issues (should be empty): []
#!/usr/bin/env groovy
import groovyx.net.http.RESTClient
import org.apache.http.*
import org.apache.http.protocol.*
import groovyx.net.http.*
@Grab(group='org.codehaus.groovy.modules.http-builder', module='http-builder', version='0.7.1')
def scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent
def token = new File("$scriptDir/github.token").text.trim()
// initialze a new builder and give a default URL
def github = new RESTClient( 'https://api.github.com' ).with {
client.addRequestInterceptor(
[process: { HttpRequest request, HttpContext context ->
// using httpbuilders auth mechanism doesn't work, do it manually
request.setHeader("Authorization", "token $token")
}] as HttpRequestInterceptor
)
client.params.setIntParameter('http.connection.timeout', 5000)
client.params.setIntParameter('http.socket.timeout', 5000)
delegate
}
def getNext(response) {
def next = null
for (link in response.getHeaders('Link')?.value[0]?.split(',')) {
def matcher = link =~ /< (.*)\?page=(\d+)>; rel="next"/
if (matcher.matches()) {
next = [url:matcher[0][1], page:matcher[0][2]]
}
}
next
}
def fromGithub = { path ->
def resp = github.get(path: path, headers:['User-Agent':'Groovy'])
def data = resp.data
def next = getNext(resp)
while (next) {
resp = github.get(path: next.url, query:[page: next.page], headers:['User-Agent':'Groovy'])
data += resp.data
next = getNext(resp)
}
data
}
def addRepoToTeam = { repo, team ->
def resp = github.put(path: "/teams/$team.id/repos/openmrs/$repo.name",
headers:['User-Agent':'Groovy'])
}
def editRepo = { repo ->
def body = """{"name":"$repo.name", "has_issues":false, "has_wiki":false}"""
def resp = github.patch(path: "/repos/openmrs/$repo.name", body: body,
requestContentType:ContentType.URLENC, headers:['User-Agent':'Groovy'])
}
repos = fromGithub('/orgs/openmrs/repos')
teams = fromGithub('/orgs/openmrs/teams').findAll{ it.name != 'Transfer Team' && it.name != 'Release-test' }
println "OpenMRS org has ${repos.size()} repos"
for (team in teams) {
teamRepoNames = fromGithub("/teams/$team.id/repos").collect{ it.name }
missing = repos.findAll{ !(it.name in teamRepoNames) && it.name != 'openmrs-core' }
print "$team.name team has ${teamRepoNames.size()} repos, "
println missing.size() > 0 ? "missing from these: ${missing.collect{it.name}}" : "missing none"
for (repo in missing) {
print "fixing..."
addRepoToTeam(repo, team)
println "done."
}
}
wikisOrIssues = repos.findAll{ it.has_wiki || it.has_issues }
println "wikis or issues (should be empty): ${wikisOrIssues.collect{it.name}}"
for (repo in wikisOrIssues) {
print "fixing..."
editRepo(repo)
println "done."
}
In the process of upgrading the software license for OpenMRS, it seems like a good time to review how we got here and why we’re changing our license. Here’s a brief history of OpenMRS Licensing…
Ok. Let’s stop here for a second. At this point, in 2007, OpenMRS transforms from a shared, unlicensed, pile of code within a public Subversion repository into officially licensed open-source software. So, what is this OpenMRS Public License and where did it come from? With the help of local lawyers, we hired a lawyer with expertise in open source licensing and described our goals:
The lawyer took these goals, reviewed the available open source licenses at the time, and suggested the Mozilla Public License 1.1 with a few tweaks to fit our specific needs. So, we created the OpenMRS Public License 1.0 as a slightly modified version of MPL 1.1 and applied it to all of our code.
In 2013, with some hard work by Paul and help from OSI, Luis Villa, and some other lawyers, we discover that the Mozilla Public License 2.0 with a disclaimer could meet all of our needs, so we proposed the idea to the community. Overall, people in the community are pleased to see us adopting an OSI-approved license.
Okay, OpenMRS on an OSI-approved license… at last. That’s cool. But why Mozilla Public License 2.0 (MPL 2.0)?

See our license at http://openmrs.org/license.
I was recently reminded of the quote that so often is (mis)attributed to St. Francis of Assisi:
Without getting religious, I believe and hope OpenMRS follows this dictum: spend more time & effort improving health in resource poor environments than we spend talking about it. And, for OpenMRS, we choose to do our work openly. Why? Because “Open Source” is not so much about sharing code as it is about sharing experiences, teaching & learning from each other, and embracing collaboration & open methodologies… in short, open behavior. It took a while to learn that distinction. Now when I hear people fussing over code, it feels petty. One of the most important lessons that I’ve learned from the OpenMRS Community:
We could switch to a completely different programming language or a new shiny platform could (and likely will) come along some day that’s far better than what we’ve done. All of our precious code will fade away. That’s fine. As long as we have our people. Are you running a development shop? Have you calculated how much you’ve invested in your “product” and found yourself thinking about the code? Wrong. Your investment is in people. Give me the choice between good code and a good coder and I will take the coder 100 times out of 100. In the end, code is just a tool… property. And property can be replaced. Love your coders. Treat them well and invest in them. Life will be good. And while you’re doing that, consider working openly.
As we work to organize & improve the development work we are doing for OpenMRS, we’re starting to use (and sometimes misuse) a growing list of terminology. I thought I’d dump them out here (to get them out of my head, to be able to come back & look at them, and maybe to get comments from folks on what we’ve got wrong). Many of these are drawn from our (growing) experience with agile methodology. Since I’m not taking the time to look up “official” definitions, I’m sure these are imprecise and I wouldn’t be surprised to learn that we’re doing what others were doing 10 years ago. 🙂
What gets done:
How it gets done:
Who does the work:
Thoughts or comments are welcome.
I wanted to check out OpenMRS trunk and a couple branches from the subversion repository within the same local copy so that I could apply a set of changes to trunk and backport it to a couple prior versions in a single commit. Typically, when you check out a directory, you get everything underneath. But OpenMRS trunk and the branches I wanted are only a few of the bazillion folders and files underneath http://svn.openmrs.org/openmrs/. Checking out all of those folders would take a long time, place an unnecessary tax on the OpenMRS repository, and end up transferring a bazillion files that I didn’t need. While it’s not obvious, there is a way to do this with Subversion. The answer is in Subversions sparse directories feature. Thank you, stackoverflow! Here’s how I checked out only a subset of folders (trunk and a couple branches) from subversion:
$ svn co --depth empty http://svn.openmrs.org/openmrs
$ svn update --set-depth infinity openmrs/trunk
$ svn update --set-depth empty openmrs/branches
$ svn update --set-depth infinity openmrs/branches/1.8.x
$ svn update --set-depth infinity openmrs/branches/1.9.x
I just ran into a handy little utility: jsonpretty.
$sudo gem install json jsonpretty
While
$curl -i http://localhost:8081/openmrs-standalone/ws/rest/v1/catalog
gives you something like this:
{"catalog":[{"name":"Cohort","operations":[{"name":"GET http://localhost:8081/openmrs/ws/rest/v1/cohort?q","description":"Fetch all non-retired that match this parameter"},{"name":"GET http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}","description":"Fetch by unique uuid"},{"name":"GET http://localhost:8081/openmrs/ws/rest/v1/cohort","description":"Fetch all non-retired"},{"name":"POST http://localhost:8081/openmrs/ws/rest/v1/cohort","description":"Create with properties in request"},{"name":"POST http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}","description":"Edit with given uuid, only modifying properties in request"},{"name":"DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}?!purge","description":"Delete this object from the database"},{"name":"DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}?purge","description":"Delete this object from the database"}],"url":"http://localhost:8081/openmrs/ws/rest/v1/cohort","representations":[{"name":"ref","properties":["uuid","display","links"]},{"name":"default","properties":["uuid","name","description","voided","memberIds","links"]},{"name":"full","properties":["uuid","name","description","memberIds","voided","auditInfo","links"]}]},{"name":"CohortMember","operations":[{"name":"GET http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members/{uuid}","description":"Fetch by unique uuid"},{"name":"GET http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members","description":"Fetch all non-retired"},{"name":"POST http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members","description":"Create with properties in request"},{"name":"POST http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members/{uuid}","description":"Edit with given uuid, only modifying properties in request"},{"name":"DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members/{uuid}?!purge","description":"Delete this object from the database"},{"name":"DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members/{uuid}?purge","description":"Delete this object from the database"}],...
adding jsonpretty:
$curl -i http://localhost:8081/openmrs-standalone/ws/rest/v1/catalog | jsonpretty
gets you something much prettier:
{
"catalog": [
{
"name": "Cohort",
"operations": [
{
"name": "GET http://localhost:8081/openmrs/ws/rest/v1/cohort?q",
"description": "Fetch all non-retired that match this parameter"
},
{
"name": "GET http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}",
"description": "Fetch by unique uuid"
},
{
"name": "GET http://localhost:8081/openmrs/ws/rest/v1/cohort",
"description": "Fetch all non-retired"
},
{
"name": "POST http://localhost:8081/openmrs/ws/rest/v1/cohort",
"description": "Create with properties in request"
},
{
"name": "POST http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}",
"description": "Edit with given uuid, only modifying properties in request"
},
{
"name": "DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}?!purge",
"description": "Delete this object from the database"
},
{
"name": "DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}?purge",
"description": "Delete this object from the database"
}
],
"url": "http://localhost:8081/openmrs/ws/rest/v1/cohort",
"representations": [
{
"name": "ref",
"properties": [
"uuid",
"display",
"links"
]
},
{
"name": "default",
"properties": [
"uuid",
"name",
"description",
"voided",
"memberIds",
"links"
]
},
{
"name": "full",
"properties": [
"uuid",
"name",
"description",
"memberIds",
"voided",
"auditInfo",
"links"
]
}
]
},
...
Nice! Thank you jsonpretty! 🙂