MigrationHelper.java

/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package org.openmrs.migration;

import java.io.IOException;
import java.io.StringReader;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.Location;
import org.openmrs.Patient;
import org.openmrs.PatientIdentifier;
import org.openmrs.PatientIdentifierType;
import org.openmrs.PatientProgram;
import org.openmrs.PatientState;
import org.openmrs.Person;
import org.openmrs.PersonName;
import org.openmrs.Program;
import org.openmrs.ProgramWorkflow;
import org.openmrs.ProgramWorkflowState;
import org.openmrs.Relationship;
import org.openmrs.RelationshipType;
import org.openmrs.Role;
import org.openmrs.User;
import org.openmrs.api.LocationService;
import org.openmrs.api.PatientService;
import org.openmrs.api.PersonService;
import org.openmrs.api.ProgramWorkflowService;
import org.openmrs.api.UserService;
import org.openmrs.api.context.Context;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class MigrationHelper {
	
	protected final static Log log = LogFactory.getLog(MigrationHelper.class);
	
	static DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
	
	static DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	
	public static Date parseDate(String s) throws ParseException {
		if (s == null || s.length() == 0) {
			return null;
		} else {
			if (s.length() == 10) {
				s += " 00:00:00";
			}
			return df.parse(s);
		}
	}
	
	public static Document parseXml(String xml) throws ParserConfigurationException {
		DocumentBuilder builder = factory.newDocumentBuilder();
		try {
			return builder.parse(new InputSource(new StringReader(xml)));
		}
		catch (IOException ex) {
			return null;
		}
		catch (SAXException e) {
			return null;
		}
	}
	
	private static void findNodesNamed(Node node, String lookForName, Collection<Node> ret) {
		if (node.getNodeName().equals(lookForName)) {
			ret.add(node);
		} else {
			NodeList list = node.getChildNodes();
			for (int i = 0; i < list.getLength(); ++i) {
				findNodesNamed(list.item(i), lookForName, ret);
			}
		}
	}
	
	/**
	 * Takes XML like: <something> <user date_changed="2001-03-06 08:46:53.0"
	 * date_created="2001-03-06 08:46:53.0" username="hamish@mit.edu" first_name="Hamish"
	 * last_name="Fraser" user_id="2001"/> </something> Returns the number of users added
	 */
	public static int importUsers(Document document) throws ParseException {
		int ret = 0;
		Random rand = new Random();
		UserService us = Context.getUserService();
		
		List<Node> toAdd = new ArrayList<Node>();
		findNodesNamed(document, "user", toAdd);
		for (Node node : toAdd) {
			Element e = (Element) node;
			String username = e.getAttribute("username");
			if (username == null || username.length() == 0) {
				throw new IllegalArgumentException("each <user /> element must define a user_name attribute");
			}
			if (us.getUserByUsername(username) != null) {
				continue;
			}
			User user = new User();
			user.setPerson(new Person());
			PersonName pn = new PersonName(e.getAttribute("first_name"), "", e.getAttribute("last_name"));
			user.addName(pn);
			user.setUsername(username);
			user.setDateCreated(parseDate(e.getAttribute("date_created")));
			user.setDateChanged(parseDate(e.getAttribute("date_changed")));
			
			// Generate a temporary password: 8-12 random characters
			String pass = null;
			{
				int length = rand.nextInt(4) + 8;
				char[] password = new char[length];
				for (int x = 0; x < length; x++) {
					int randDecimalAsciiVal = rand.nextInt(93) + 33;
					password[x] = (char) randDecimalAsciiVal;
				}
				pass = new String(password);
			}
			us.saveUser(user, pass);
			++ret;
		}
		return ret;
	}
	
	/**
	 * Takes XML like: <something> <location name="Cerca-la-Source"/> </something> returns the
	 * number of locations added
	 */
	public static int importLocations(Document document) {
		int ret = 0;
		LocationService ls = Context.getLocationService();
		List<Node> toAdd = new ArrayList<Node>();
		findNodesNamed(document, "location", toAdd);
		for (Node node : toAdd) {
			Element e = (Element) node;
			String name = e.getAttribute("name");
			if (name == null || name.length() == 0) {
				throw new IllegalArgumentException("each <location /> element must define a name attribute");
			}
			if (ls.getLocation(name) != null) {
				continue;
			}
			Location location = new Location();
			location.setName(name);
			
			ls.saveLocation(location);
			++ret;
		}
		return ret;
	}
	
	/**
	 * Takes a list of Strings of the format RELATIONSHIP:<user last name>,<user first
	 * name>,<relationship type name>,<patient identifier type name>,<identifier> so if user hfraser
	 * if the cardiologist of the patient with patient_id 8039 in PIH's old emr, then:
	 * RELATIONSHIP:hfraser,Cardiologist,HIV-EMRV1,8039 (the "RELATIONSHIP:" is not actually
	 * necessary. Anything before and including the first : will be dropped If autoCreateUsers is
	 * true, and no user exists with the given username, one will be created. If autoAddRole is
	 * true, then whenever a user is auto-created, if a role exists with the same name as
	 * relationshipType.name, then the user will be added to that role
	 */
	public static int importRelationships(Collection<String> relationships, boolean autoCreateUsers, boolean autoAddRole) {
		PatientService ps = Context.getPatientService();
		UserService us = Context.getUserService();
		PersonService personService = Context.getPersonService();
		List<Relationship> relsToAdd = new ArrayList<Relationship>();
		Random rand = new Random();
		for (String s : relationships) {
			if (s.indexOf(":") >= 0)
				s = s.substring(s.indexOf(":") + 1);
			String[] ss = s.split(",");
			if (ss.length < 5)
				throw new IllegalArgumentException("The line '" + s + "' is in the wrong format");
			String userLastName = ss[0];
			String userFirstName = ss[1];
			String username = (userFirstName + userLastName).replaceAll(" ", "");
			String relationshipType = ss[2];
			String identifierType = ss[3];
			String identifier = ss[4];
			User user = null;
			{ // first try looking for non-voided users
				List<User> users = us.getUsersByName(userFirstName, userLastName, false);
				if (users.size() == 1)
					user = users.get(0);
				else if (users.size() > 1) {
					throw new IllegalArgumentException("Found " + users.size() + " users named '" + userLastName + ", "
					        + userFirstName + "'");
				}
			}
			if (user == null) {
				// next try looking for voided users
				List<User> users = us.getUsersByName(userFirstName, userLastName, false);
				if (users.size() == 1)
					user = users.get(0);
				else if (users.size() > 1) {
					throw new IllegalArgumentException("Found " + users.size() + " voided users named '" + userLastName
					        + ", " + userFirstName + "'");
				}
			}
			if (user == null && autoCreateUsers) {
				user = new User();
				user.setPerson(new Person());
				PersonName pn = new PersonName(userFirstName, "", userLastName);
				user.addName(pn);
				user.setUsername(username);
				// Generate a temporary password: 8-12 random characters
				String pass = null;
				{
					int length = rand.nextInt(4) + 8;
					char[] password = new char[length];
					for (int x = 0; x < length; x++) {
						int randDecimalAsciiVal = rand.nextInt(93) + 33;
						password[x] = (char) randDecimalAsciiVal;
					}
					pass = new String(password);
				}
				if (autoAddRole) {
					Role role = us.getRole(relationshipType);
					if (role != null)
						user.addRole(role);
				}
				us.saveUser(user, pass);
			}
			if (user == null)
				throw new IllegalArgumentException("Can't find user '" + userLastName + ", " + userFirstName + "'");
			Person person = personService.getPerson(user.getUserId());
			
			RelationshipType relationship = personService.getRelationshipTypeByName(relationshipType);
			PatientIdentifierType pit = ps.getPatientIdentifierTypeByName(identifierType);
			List<PatientIdentifier> found = ps.getPatientIdentifiers(identifier, Collections.singletonList(pit), null, null,
			    null);
			if (found.size() != 1)
				throw new IllegalArgumentException("Found " + found.size() + " patients with identifier '" + identifier
				        + "' of type " + identifierType);
			Person relative = personService.getPerson(found.get(0).getPatient().getPatientId());
			Relationship rel = new Relationship();
			rel.setPersonA(person);
			rel.setRelationshipType(relationship);
			rel.setPersonB(relative);
			relsToAdd.add(rel);
		}
		int addedSoFar = 0;
		for (Relationship rel : relsToAdd) {
			personService.saveRelationship(rel);
			++addedSoFar;
		}
		return addedSoFar;
	}
	
	public static int importProgramsAndStatuses(List<String> programWorkflow) throws ParseException {
		ProgramWorkflowService pws = Context.getProgramWorkflowService();
		PatientService ps = Context.getPatientService();
		List<PatientProgram> patientPrograms = new ArrayList<PatientProgram>();
		//List<PatientState> patientStates = new ArrayList<PatientState>();
		Map<String, PatientProgram> knownPatientPrograms = new HashMap<String, PatientProgram>();
		Map<String, Program> programsByName = new HashMap<String, Program>();
		for (Program program : pws.getAllPrograms()) {
			programsByName.put(program.getConcept().getName(Context.getLocale(), false).getName(), program);
		}
		for (String s : programWorkflow) {
			// ENROLLMENT:HIVEMR-V1,9266,IMB HIV PROGRAM,2005-08-25,
			log.debug(s);
			if (s.startsWith("ENROLLMENT:")) {
				s = s.substring(s.indexOf(":") + 1);
				String[] temp = s.split(",");
				PatientIdentifierType pit = ps.getPatientIdentifierTypeByName(temp[0]);
				String identifier = temp[1];
				List<PatientIdentifier> pis = ps.getPatientIdentifiers(identifier, Collections.singletonList(pit), null,
				    null, null);
				if (pis.size() != 1)
					throw new IllegalArgumentException("Found " + pis.size() + " instances of identifier " + identifier
					        + " of type " + pit);
				Patient p = pis.get(0).getPatient();
				Program program = programsByName.get(temp[2]);
				if (program == null)
					throw new RuntimeException("Couldn't find program \"" + temp[2] + "\" in " + programsByName);
				Date enrollmentDate = temp.length < 4 ? null : parseDate(temp[3]);
				Date completionDate = temp.length < 5 ? null : parseDate(temp[4]);
				PatientProgram pp = new PatientProgram();
				pp.setPatient(p);
				pp.setProgram(program);
				pp.setDateEnrolled(enrollmentDate);
				pp.setDateCompleted(completionDate);
				patientPrograms.add(pp);
				knownPatientPrograms.put(temp[0] + "," + temp[1] + "," + temp[2], pp); // "HIVEMR-V1,9266,IMB HIV PROGRAM"
			} else if (s.startsWith("STATUS:")) {
				// STATUS:HIVEMR-V1,9266,IMB HIV PROGRAM,TREATMENT STATUS,ACTIVE,2005-08-25,,
				s = s.substring(s.indexOf(":") + 1);
				String[] temp = s.split(",");
				/* We're using a cache of 'knownPatientPrograms' instead of the following commented code
				PatientIdentifierType pit = ps.getPatientIdentifierType(temp[0]);
				String identifier = temp[1];
				List<PatientIdentifier> pis = ps.getPatientIdentifiers(identifier, pit);
				if (pis.size() != 1)
					throw new IllegalArgumentException("Found " + pis.size() + " instances of identifier " + identifier + " of type " + pit);
				Patient p = pis.get(0).getPatient();
				*/
				Program program = programsByName.get(temp[2]);
				if (program == null)
					throw new RuntimeException("Couldn't find program \"" + temp[2] + "\" in " + programsByName);
				//ProgramWorkflow wf = pws.getWorkflow(program, temp[3]);
				ProgramWorkflow wf = program.getWorkflowByName(temp[3]);
				if (wf == null)
					throw new RuntimeException("Couldn't find workflow \"" + temp[3] + "\" for program " + program + " (in "
					        + program.getAllWorkflows() + ")");
				//ProgramWorkflowState st = pws.getState(wf, temp[4]);
				ProgramWorkflowState st = wf.getStateByName(temp[4]);
				if (st == null)
					throw new RuntimeException("Couldn't find state \"" + temp[4] + "\" for workflow " + wf + " (in "
					        + wf.getStates() + ")");
				Date startDate = temp.length < 6 ? null : parseDate(temp[5]);
				Date endDate = temp.length < 7 ? null : parseDate(temp[6]);
				PatientState state = new PatientState();
				PatientProgram pp = knownPatientPrograms.get(temp[0] + "," + temp[1] + "," + temp[2]);
				state.setPatientProgram(pp);
				state.setState(st);
				state.setStartDate(startDate);
				state.setEndDate(endDate);
				pp.getStates().add(state);
			}
		}
		int numAdded = 0;
		
		for (PatientProgram pp : knownPatientPrograms.values()) {
			pws.savePatientProgram(pp);
			++numAdded;
		}
		return numAdded;
	}
	
}