PatientProgram.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;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.openmrs.util.OpenmrsUtil;

/**
 * PatientProgram
 */
public class PatientProgram extends BaseOpenmrsData implements java.io.Serializable {
	
	public static final long serialVersionUID = 0L;
	
	// ******************
	// Properties
	// ******************
	
	private Integer patientProgramId;
	
	private Patient patient;
	
	private Program program;
	
	private Location location;
	
	private Date dateEnrolled;
	
	private Date dateCompleted;
	
	private Concept outcome;
	
	private Set<PatientState> states = new HashSet<PatientState>();
	
	// ******************
	// Constructors
	// ******************
	
	/** Default Constructor */
	public PatientProgram() {
	}
	
	/** Constructor with id */
	public PatientProgram(Integer patientProgramId) {
		setPatientProgramId(patientProgramId);
	}
	
	/**
	 * Does a mostly-shallow copy of this PatientProgram. Does not copy patientProgramId. The
	 * 'states' property will be deep-copied.
	 * 
	 * @return a shallow copy of this PatientProgram
	 */
	public PatientProgram copy() {
		return copyHelper(new PatientProgram());
	}
	
	/**
	 * The purpose of this method is to allow subclasses of PatientProgram to delegate a portion of
	 * their copy() method back to the superclass, in case the base class implementation changes.
	 * 
	 * @param target a PatientProgram that will have the state of <code>this</code> copied into it
	 * @return the PatientProgram that was passed in, with state copied into it
	 */
	protected PatientProgram copyHelper(PatientProgram target) {
		target.setPatient(this.getPatient());
		target.setProgram(this.getProgram());
		target.setLocation(this.getLocation());
		target.setDateEnrolled(this.getDateEnrolled());
		target.setDateCompleted(target.getDateCompleted());
		Set<PatientState> statesCopy = new HashSet<PatientState>();
		if (this.getStates() != null) {
			for (PatientState s : this.getStates()) {
				PatientState stateCopy = s.copy();
				stateCopy.setPatientProgram(target);
				statesCopy.add(stateCopy);
			}
		}
		target.setStates(statesCopy);
		target.setCreator(this.getCreator());
		target.setDateCreated(this.getDateCreated());
		target.setChangedBy(this.getChangedBy());
		target.setDateChanged(this.getDateChanged());
		target.setVoided(this.getVoided());
		target.setVoidedBy(this.getVoidedBy());
		target.setDateVoided(this.getDateVoided());
		target.setVoidReason(this.getVoidReason());
		return target;
	}
	
	// ******************
	// Instance methods
	// ******************
	
	/**
	 * Returns true if the associated {@link Patient} is enrolled in the associated {@link Program}
	 * on the passed {@link Date}
	 * 
	 * @param onDate - Date to check for PatientProgram enrollment
	 * @return boolean - true if the associated {@link Patient} is enrolled in the associated
	 *         {@link Program} on the passed {@link Date}
	 */
	public boolean getActive(Date onDate) {
		if (onDate == null) {
			onDate = new Date();
		}
		return !getVoided() && (getDateEnrolled() == null || OpenmrsUtil.compare(getDateEnrolled(), onDate) <= 0)
		        && (getDateCompleted() == null || OpenmrsUtil.compare(getDateCompleted(), onDate) > 0);
	}
	
	/**
	 * Returns true if the associated {@link Patient} is currently enrolled in the associated
	 * {@link Program}
	 * 
	 * @return boolean - true if the associated {@link Patient} is currently enrolled in the
	 *         associated {@link Program}
	 */
	public boolean getActive() {
		return getActive(null);
	}
	
	/**
	 * Returns the {@link PatientState} associated with this PatientProgram that has an id that
	 * matches the passed <code>patientStateId</code>
	 * 
	 * @param patientStateId - The identifier to use to lookup a {@link PatientState}
	 * @return PatientState that has an id that matches the passed <code>patientStateId</code>
	 */
	public PatientState getPatientState(Integer patientStateId) {
		for (PatientState s : getStates()) {
			if (s.getPatientStateId() != null && s.getPatientStateId().equals(patientStateId)) {
				return s;
			}
		}
		return null;
	}
	
	/**
	 * Attempts to transition the PatientProgram to the passed {@link ProgramWorkflowState} on the
	 * passed {@link Date} by ending the most recent {@link PatientState} in the
	 * {@link PatientProgram} and creating a new one with the passed {@link ProgramWorkflowState}
	 * This will throw an IllegalArgumentException if the transition is invalid
	 * 
	 * @param programWorkflowState - The {@link ProgramWorkflowState} to transition to
	 * @param onDate - The {@link Date} of the transition
	 * @throws IllegalArgumentException
	 */
	public void transitionToState(ProgramWorkflowState programWorkflowState, Date onDate) {
		PatientState lastState = getCurrentState(programWorkflowState.getProgramWorkflow());
		if (lastState != null && onDate == null) {
			throw new IllegalArgumentException("You can't change from a non-null state without giving a change date");
		}
		if (lastState != null && lastState.getEndDate() != null) {
			throw new IllegalArgumentException("You can't change out of a state that has an end date already");
		}
		if (lastState != null && lastState.getStartDate() != null
		        && OpenmrsUtil.compare(lastState.getStartDate(), onDate) > 0) {
			throw new IllegalArgumentException("You can't change out of a state before that state started");
		}
		if (lastState != null
		        && !programWorkflowState.getProgramWorkflow().isLegalTransition(lastState.getState(), programWorkflowState)) {
			throw new IllegalArgumentException("You can't change from state " + lastState.getState() + " to "
			        + programWorkflowState);
		}
		if (lastState != null) {
			lastState.setEndDate(onDate);
		}
		
		PatientState newState = new PatientState();
		newState.setPatientProgram(this);
		newState.setState(programWorkflowState);
		newState.setStartDate(onDate);
		
		if (programWorkflowState.getTerminal() == Boolean.TRUE) {
			setDateCompleted(onDate);
		}
		
		getStates().add(newState);
	}
	
	/**
	 * Attempts to void the latest {@link PatientState} in the {@link PatientProgram} If earlier
	 * PatientStates exist, it will try to reset the endDate to null so that the next latest state
	 * becomes the current {@link PatientState}
	 * 
	 * @param workflow - The {@link ProgramWorkflow} whose last {@link PatientState} within the
	 *            current {@link PatientProgram} we want to void
	 * @param voidBy - The user who is voiding the {@link PatientState}
	 * @param voidDate - The date to void the {@link PatientState}
	 * @param voidReason - The reason for voiding the {@link PatientState}
	 * @should void state with endDate null if startDates equal
	 */
	public void voidLastState(ProgramWorkflow workflow, User voidBy, Date voidDate, String voidReason) {
		List<PatientState> states = statesInWorkflow(workflow, false);
		if (voidDate == null) {
			voidDate = new Date();
		}
		PatientState last = null;
		PatientState nextToLast = null;
		if (states.size() > 0) {
			last = states.get(states.size() - 1);
		}
		if (states.size() > 1) {
			nextToLast = states.get(states.size() - 2);
		}
		if (last != null) {
			last.setVoided(true);
			last.setVoidedBy(voidBy);
			last.setDateVoided(voidDate);
			last.setVoidReason(voidReason);
		}
		if (nextToLast != null && nextToLast.getEndDate() != null) {
			nextToLast.setEndDate(null);
			nextToLast.setDateChanged(voidDate);
			nextToLast.setChangedBy(voidBy);
		}
	}
	
	/**
	 * Returns the current {@link PatientState} for the passed {@link ProgramWorkflow} within this
	 * {@link PatientProgram}.
	 * 
	 * @param programWorkflow The ProgramWorkflow whose current {@link PatientState} we want to
	 *            retrieve
	 * @return PatientState The current {@link PatientState} for the passed {@link ProgramWorkflow}
	 *         within this {@link PatientProgram}
	 */
	public PatientState getCurrentState(ProgramWorkflow programWorkflow) {
		Date now = new Date();
		PatientState currentState = null;
		
		for (PatientState state : getSortedStates()) {
			//states are sorted with the most current state at the last position
			if ((programWorkflow == null || state.getState().getProgramWorkflow().equals(programWorkflow))
			        && state.getActive(now)) {
				currentState = state;
			}
		}
		return currentState;
	}
	
	/**
	 * @deprecated use {@link #getCurrentState(ProgramWorkflow)}
	 */
	public PatientState getCurrentState() {
		return getCurrentState(null);
	}
	
	/**
	 * Returns a Set<PatientState> of all current {@link PatientState}s for the
	 * {@link PatientProgram}
	 * 
	 * @return Set<PatientState> of all current {@link PatientState}s for the {@link PatientProgram}
	 */
	public Set<PatientState> getCurrentStates() {
		Set<PatientState> ret = new HashSet<PatientState>();
		Date now = new Date();
		for (PatientState state : getStates()) {
			if (state.getActive(now)) {
				ret.add(state);
			}
		}
		return ret;
	}
	
	/**
	 * Returns a List<PatientState> of all {@link PatientState}s in the passed
	 * {@link ProgramWorkflow} for the {@link PatientProgram}
	 * 
	 * @param programWorkflow - The {@link ProgramWorkflow} to check
	 * @param includeVoided - If true, return voided {@link PatientState}s in the returned
	 *            {@link List}
	 * @return List<PatientState> of all {@link PatientState}s in the passed {@link ProgramWorkflow}
	 *         for the {@link PatientProgram}
	 */
	public List<PatientState> statesInWorkflow(ProgramWorkflow programWorkflow, boolean includeVoided) {
		List<PatientState> ret = new ArrayList<PatientState>();
		for (PatientState st : getSortedStates()) {
			if (st.getState().getProgramWorkflow().equals(programWorkflow) && (includeVoided || !st.getVoided())) {
				ret.add(st);
			}
		}
		return ret;
	}
	
	/** @see Object#toString() */
	public String toString() {
		return "PatientProgram(id=" + getPatientProgramId() + ", patient=" + getPatient() + ", program=" + getProgram()
		        + ")";
	}
	
	// ******************
	// Property Access
	// ******************
	
	public Concept getOutcome() {
		return outcome;
	}
	
	public void setOutcome(Concept concept) {
		this.outcome = concept;
	}
	
	public Date getDateCompleted() {
		return dateCompleted;
	}
	
	public void setDateCompleted(Date dateCompleted) {
		this.dateCompleted = dateCompleted;
	}
	
	public Date getDateEnrolled() {
		return dateEnrolled;
	}
	
	public void setDateEnrolled(Date dateEnrolled) {
		this.dateEnrolled = dateEnrolled;
	}
	
	public Patient getPatient() {
		return patient;
	}
	
	public void setPatient(Patient patient) {
		this.patient = patient;
	}
	
	public Integer getPatientProgramId() {
		return patientProgramId;
	}
	
	public void setPatientProgramId(Integer patientProgramId) {
		this.patientProgramId = patientProgramId;
	}
	
	public Program getProgram() {
		return program;
	}
	
	public void setProgram(Program program) {
		this.program = program;
	}
	
	public Set<PatientState> getStates() {
		return states;
	}
	
	public void setStates(Set<PatientState> states) {
		this.states = states;
	}
	
	/**
	 * @since 1.5
	 * @see org.openmrs.OpenmrsObject#getId()
	 */
	public Integer getId() {
		return getPatientProgramId();
	}
	
	/**
	 * @since 1.5
	 * @see org.openmrs.OpenmrsObject#setId(java.lang.Integer)
	 */
	public void setId(Integer id) {
		setPatientProgramId(id);
	}
	
	/**
	 * @since 1.8
	 * @return the location
	 */
	public Location getLocation() {
		return location;
	}
	
	/**
	 * @since 1.8
	 * @param location the location to set
	 */
	public void setLocation(Location location) {
		this.location = location;
	}
	
	/**
	 * @return states sorted by {@link PatientState#compareTo(PatientState)}
	 */
	private List<PatientState> getSortedStates() {
		List<PatientState> sortedStates = new ArrayList<PatientState>(getStates());
		Collections.sort(sortedStates);
		return sortedStates;
	}
}