org.openmrs.module.providermanagement.api.impl.ProviderManagementServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.providermanagement.api.impl.ProviderManagementServiceImpl.java

Source

/**
 * 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.module.providermanagement.api.impl;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.ListUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.exception.ConstraintViolationException;
import org.openmrs.Patient;
import org.openmrs.Person;
import org.openmrs.PersonAddress;
import org.openmrs.PersonAttribute;
import org.openmrs.Relationship;
import org.openmrs.RelationshipType;
import org.openmrs.api.APIException;
import org.openmrs.api.context.Context;
import org.openmrs.api.impl.BaseOpenmrsService;
import org.openmrs.module.providermanagement.Provider;
import org.openmrs.module.providermanagement.ProviderManagementConstants;
import org.openmrs.module.providermanagement.ProviderManagementUtils;
import org.openmrs.module.providermanagement.ProviderRole;
import org.openmrs.module.providermanagement.api.ProviderManagementService;
import org.openmrs.module.providermanagement.api.db.ProviderManagementDAO;
import org.openmrs.module.providermanagement.comparator.PersonByFirstNameComparator;
import org.openmrs.module.providermanagement.exception.DateCannotBeInFutureException;
import org.openmrs.module.providermanagement.exception.InvalidRelationshipTypeException;
import org.openmrs.module.providermanagement.exception.InvalidSupervisorException;
import org.openmrs.module.providermanagement.exception.PatientAlreadyAssignedToProviderException;
import org.openmrs.module.providermanagement.exception.PatientNotAssignedToProviderException;
import org.openmrs.module.providermanagement.exception.PersonIsNotProviderException;
import org.openmrs.module.providermanagement.exception.ProviderAlreadyAssignedToSupervisorException;
import org.openmrs.module.providermanagement.exception.ProviderDoesNotSupportRelationshipTypeException;
import org.openmrs.module.providermanagement.exception.ProviderNotAssignedToSupervisorException;
import org.openmrs.module.providermanagement.exception.ProviderRoleInUseException;
import org.openmrs.module.providermanagement.exception.SourceProviderSameAsDestinationProviderException;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * It is a default implementation of {@link ProviderManagementService}.
 */
public class ProviderManagementServiceImpl extends BaseOpenmrsService implements ProviderManagementService {

    // TODO: (??? --not sure what this comment means anymore?) add checks to make sure person is not voided automatically when appropriate (in the assignment classes?)

    protected final Log log = LogFactory.getLog(this.getClass());

    private ProviderManagementDAO dao;

    private static RelationshipType supervisorRelationshipType = null;

    /**
      * @param dao the dao to set
      */
    public void setDao(ProviderManagementDAO dao) {
        this.dao = dao;
    }

    /**
     * @return the dao
     */
    public ProviderManagementDAO getDao() {
        return dao;
    }

    @Override
    @Transactional(readOnly = true)
    public List<ProviderRole> getAllProviderRoles(boolean includeRetired) {
        return dao.getAllProviderRoles(includeRetired);
    }

    @Override
    @Transactional(readOnly = true)
    public ProviderRole getProviderRole(Integer id) {
        return dao.getProviderRole(id);
    }

    @Override
    @Transactional(readOnly = true)
    public ProviderRole getProviderRoleByUuid(String uuid) {
        return dao.getProviderRoleByUuid(uuid);
    }

    @Override
    @Transactional(readOnly = true)
    public List<ProviderRole> getProviderRolesByRelationshipType(RelationshipType relationshipType) {
        if (relationshipType == null) {
            throw new APIException("relationshipType cannot be null");
        } else {
            return dao.getProviderRolesByRelationshipType(relationshipType);
        }
    }

    @Override
    @Transactional(readOnly = true)
    public List<ProviderRole> getProviderRolesBySuperviseeProviderRole(ProviderRole providerRole) {
        if (providerRole == null) {
            throw new APIException("providerRole cannot be null");
        } else {
            return dao.getProviderRolesBySuperviseeProviderRole(providerRole);
        }
    }

    @Override
    @Transactional
    public ProviderRole saveProviderRole(ProviderRole role) {
        return dao.saveProviderRole(role);
    }

    @Override
    @Transactional
    public void retireProviderRole(ProviderRole role, String reason) {
        // BaseRetireHandler handles retiring the object
        dao.saveProviderRole(role);
    }

    @Override
    @Transactional
    public void unretireProviderRole(ProviderRole role) {
        // BaseUnretireHandler handles unretiring the object
        dao.saveProviderRole(role);
    }

    @Override
    @Transactional
    public void purgeProviderRole(ProviderRole role) throws ProviderRoleInUseException {

        // first, remove this role as supervisee from any roles that can supervise it
        for (ProviderRole r : getProviderRolesBySuperviseeProviderRole(role)) {
            r.getSuperviseeProviderRoles().remove(role);
            Context.getService(ProviderManagementService.class).saveProviderRole(r); // call through service so AOP save handler picks this up
        }

        try {
            dao.deleteProviderRole(role);
            Context.flushSession(); // shouldn't really have to do this, but we do to force a commit so that the exception will be thrown if necessary
        } catch (ConstraintViolationException e) {
            throw new ProviderRoleInUseException(
                    "Cannot purge provider role. Most likely it is currently linked to an existing provider ", e);
        }

    }

    @Override
    @Transactional(readOnly = true)
    public List<RelationshipType> getAllProviderRoleRelationshipTypes(boolean includeRetired) {

        Set<RelationshipType> relationshipTypes = new HashSet<RelationshipType>();

        for (ProviderRole providerRole : getAllProviderRoles(includeRetired)) {

            if (includeRetired) {
                relationshipTypes.addAll(providerRole.getRelationshipTypes());
            }
            // filter out any retired relationships
            else {
                relationshipTypes
                        .addAll(CollectionUtils.select(providerRole.getRelationshipTypes(), new Predicate() {
                            @Override
                            public boolean evaluate(Object relationshipType) {
                                return !((RelationshipType) relationshipType).getRetired();
                            }
                        }));
            }
        }

        return new ArrayList<RelationshipType>(relationshipTypes);
    }

    @Override
    public List<Person> getProvidersAsPersons(String query, List<ProviderRole> providerRoles,
            Boolean includeRetired) {

        // return empty list if no query
        if (query == null || query.length() == 0) {
            return new ArrayList<Person>();
        }

        List<Person> nameMatches = getProvidersAsPersons(query, null, providerRoles, includeRetired);
        List<Person> identifierMatches = getProvidersAsPersons(null, query, providerRoles, includeRetired);

        if (identifierMatches == null || identifierMatches.size() == 0) {
            return nameMatches;
        } else if (nameMatches == null || nameMatches.size() == 0) {
            return identifierMatches;
        } else {
            // do a union
            // TODO: how is the performance of this?
            nameMatches.removeAll(identifierMatches);
            identifierMatches.addAll(nameMatches);
            Collections.sort(identifierMatches, new PersonByFirstNameComparator());
            return identifierMatches;
        }
    }

    @Override
    @Transactional(readOnly = true)
    public List<Person> getProvidersAsPersons(String name, String identifier, List<ProviderRole> providerRoles,
            Boolean includeRetired) {
        if (providerRoles == null) {
            providerRoles = Collections.emptyList();
        }

        if (includeRetired == null) {
            throw new RuntimeException("include retired must be specified when searching for providers");
        }

        return getProvidersAsPersons(name, identifier, null, null, providerRoles, includeRetired);
    }

    @Override
    public List<Person> getProvidersAsPersons(String name, String identifier, PersonAddress personAddress,
            PersonAttribute personAttribute, List<ProviderRole> providerRoles, Boolean includeRetired) {
        return dao.getProviders(name, identifier, personAddress, personAttribute, providerRoles, includeRetired);
    }

    @Override
    @Transactional(readOnly = true)
    public List<ProviderRole> getProviderRoles(Person provider) {
        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        if (!isProvider(provider)) {
            // return empty list if this person is not a provider
            return new ArrayList<ProviderRole>();
        }

        // otherwise, collect all the roles associated with this provider
        // (we use a set to avoid duplicates at this point)
        Set<ProviderRole> providerRoles = new HashSet<ProviderRole>();

        Collection<Provider> providers = getProvidersByPerson(provider, false);

        for (Provider p : providers) {
            if (p.getProviderRole() != null) {
                providerRoles.add(p.getProviderRole());
            }
        }

        return new ArrayList<ProviderRole>(providerRoles);
    }

    @Override
    @Transactional
    public void assignProviderRoleToPerson(Person provider, ProviderRole role, String identifier) {

        if (provider == null) {
            throw new APIException("Cannot set provider role: provider is null");
        }

        if (role == null) {
            throw new APIException("Cannot set provider role: role is null");
        }

        if (provider.isVoided()) {
            throw new APIException("Cannot set provider role: underlying person has been voided");
        }

        if (hasRole(provider, role)) {
            // if the provider already has this role, do nothing
            return;
        }

        // create a new provider object and associate it with this person
        Provider p = new Provider();
        p.setPerson(provider);
        p.setIdentifier(identifier);
        p.setProviderRole(role);
        Context.getProviderService().saveProvider(p);
    }

    @Override
    @Transactional
    public void unassignProviderRoleFromPerson(Person provider, ProviderRole role) {

        if (provider == null) {
            throw new APIException("Cannot set provider role: provider is null");
        }

        if (role == null) {
            throw new APIException("Cannot set provider role: role is null");
        }

        if (!hasRole(provider, role)) {
            // if the provider doesn't have this role, do nothing
            return;
        }

        // note that we don't check to make sure this provider is a person

        // iterate through all the providers and retire any with the specified role
        for (Provider p : getProvidersByPerson(provider, true)) {
            if (p.getProviderRole().equals(role)) {
                Context.getProviderService().retireProvider(p,
                        "removing provider role " + role + " from " + provider);
            }
        }
    }

    @Override
    @Transactional
    public void purgeProviderRoleFromPerson(Person provider, ProviderRole role) {
        if (provider == null) {
            throw new APIException("Cannot set provider role: provider is null");
        }

        if (role == null) {
            throw new APIException("Cannot set provider role: role is null");
        }

        if (!hasRole(provider, role)) {
            // if the provider doesn't have this role, do nothing
            return;
        }

        // note that we don't check to make sure this provider is a person

        // iterate through all the providers and purge any with the specified role
        for (Provider p : getProvidersByPerson(provider, true)) {
            if (p.getProviderRole().equals(role)) {
                Context.getProviderService().purgeProvider(p);
            }
        }
    }

    @Override
    @Transactional(readOnly = true)
    public List<Person> getProvidersAsPersonsByRoles(List<ProviderRole> roles) {
        return providersToPersons(getProvidersByRoles(roles));
    }

    @Override
    @Transactional(readOnly = true)
    public List<Provider> getProvidersByRoles(List<ProviderRole> roles) {
        // not allowed to pass null or empty set here
        if (roles == null || roles.isEmpty()) {
            throw new APIException("Roles cannot be null or empty");
        }
        return dao.getProvidersByProviderRoles(roles, false);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Person> getProvidersAsPersonsByRole(ProviderRole role) {
        // not allowed to pass null here
        if (role == null) {
            throw new APIException("Role cannot be null");
        }

        List<ProviderRole> roles = new ArrayList<ProviderRole>();
        roles.add(role);
        return getProvidersAsPersonsByRoles(roles);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Person> getProvidersAsPersonsByRelationshipType(RelationshipType relationshipType) {

        if (relationshipType == null) {
            throw new APIException("Relationship type cannot be null");
        }

        // first fetch the roles that support this relationship type, then fetch all the providers with those roles
        List<ProviderRole> providerRoles = getProviderRolesByRelationshipType(relationshipType);
        if (providerRoles == null || providerRoles.size() == 0) {
            return new ArrayList<Person>(); // just return an empty list
        } else {
            return getProvidersAsPersonsByRoles(providerRoles);
        }
    }

    @Override
    @Transactional(readOnly = true)
    public List<ProviderRole> getProviderRolesThatCanSuperviseThisProvider(Person provider) {

        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        // first fetch all the roles for this provider
        List<ProviderRole> providerRoles = getProviderRoles(provider);

        // now fetch the roles that can supervise the roles this provider has
        Set<ProviderRole> providerRolesThatCanSupervise = new HashSet<ProviderRole>();

        for (ProviderRole providerRole : providerRoles) {
            List<ProviderRole> roles = getProviderRolesBySuperviseeProviderRole(providerRole);
            if (roles != null && roles.size() > 0) {
                providerRolesThatCanSupervise.addAll(roles);
            }
        }

        return new ArrayList<ProviderRole>(providerRolesThatCanSupervise);
    }

    @Override
    @Transactional(readOnly = true)
    public List<ProviderRole> getProviderRolesThatProviderCanSupervise(Person provider) {

        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        Set<ProviderRole> rolesThatProviderCanSupervise = new HashSet<ProviderRole>();

        // iterate through all the provider roles this provider supports
        for (ProviderRole role : getProviderRoles(provider)) {
            // add all roles that this role can supervise
            if (role.getSuperviseeProviderRoles() != null && role.getSuperviseeProviderRoles().size() > 0) {
                rolesThatProviderCanSupervise.addAll(role.getSuperviseeProviderRoles());
            }
        }

        return new ArrayList<ProviderRole>(rolesThatProviderCanSupervise);
    }

    @Override
    @Transactional(readOnly = true)
    public boolean isProvider(Person person) {

        if (person == null) {
            throw new APIException("Person cannot be null");
        }

        Collection<Provider> providers = getProvidersByPerson(person, true);
        return providers == null || providers.size() == 0 ? false : true;
    }

    @Override
    @Transactional(readOnly = true)
    public boolean hasRole(Person provider, ProviderRole role) {

        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        if (role == null) {
            throw new APIException("Role cannot be null");
        }

        return getProviderRoles(provider).contains(role);
    }

    @Override
    @Transactional(readOnly = true)
    public boolean supportsRelationshipType(Person provider, RelationshipType relationshipType) {

        Collection<Provider> providers = getProvidersByPerson(provider, false);

        if (providers == null || providers.size() == 0) {
            return false;
        }

        for (Provider p : providers) {
            if (supportsRelationshipType(p, relationshipType)) {
                return true;
            }
        }

        return false;
    }

    @Override
    @Transactional(readOnly = true)
    public boolean canSupervise(Person supervisor, Person supervisee) {

        if (supervisor == null) {
            throw new APIException("Supervisor cannot be null");
        }

        if (supervisee == null) {
            throw new APIException("Provider cannot be null");
        }

        // return false if supervisor and supervisee are the same person!
        if (supervisor.equals(supervisee)) {
            return false;
        }

        // get all the provider roles the supervisor can supervise
        List<ProviderRole> rolesThatProviderCanSupervisee = getProviderRolesThatProviderCanSupervise(supervisor);

        // get all the roles associated with the supervisee
        List<ProviderRole> superviseeProviderRoles = getProviderRoles(supervisee);

        return ListUtils.intersection(rolesThatProviderCanSupervisee, superviseeProviderRoles).size() > 0 ? true
                : false;
    }

    @Override
    @Transactional
    public void assignPatientToProvider(Patient patient, Person provider, RelationshipType relationshipType,
            Date date) throws ProviderDoesNotSupportRelationshipTypeException,
            PatientAlreadyAssignedToProviderException, PersonIsNotProviderException, DateCannotBeInFutureException {

        if (patient == null) {
            throw new APIException("Patient cannot be null");
        }

        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        if (relationshipType == null) {
            throw new APIException("Relationship type cannot be null");
        }

        if (patient.isVoided()) {
            throw new APIException("Patient cannot be voided");
        }

        if (provider.isVoided()) {
            throw new APIException("Provider cannot be voided");
        }

        if (!isProvider(provider)) {
            throw new PersonIsNotProviderException(provider.getPersonName() + " is not a provider");
        }

        if (!supportsRelationshipType(provider, relationshipType)) {
            throw new ProviderDoesNotSupportRelationshipTypeException(
                    provider.getPersonName() + " cannot support " + relationshipType);
        }

        // use current date if no date specified
        if (date == null) {
            date = new Date();
        }

        if (date.after(new Date())) {
            throw new DateCannotBeInFutureException("Assignment date cannot be in the future");
        }

        // test to mark sure the relationship doesn't already exist
        List<Relationship> relationships = Context.getPersonService().getRelationships(provider, patient,
                relationshipType, date);
        if (relationships != null && relationships.size() > 0) {
            throw new PatientAlreadyAssignedToProviderException(
                    patient.getPersonName() + " is already assigned to " + provider.getPersonName() + " with a "
                            + relationshipType + " relationship on " + Context.getDateFormat().format(date));
        }

        // go ahead and create the relationship
        Relationship relationship = new Relationship();
        relationship.setPersonA(provider);
        relationship.setPersonB(patient);
        relationship.setRelationshipType(relationshipType);
        relationship.setStartDate(ProviderManagementUtils.clearTimeComponent(date));
        Context.getPersonService().saveRelationship(relationship);
    }

    @Override
    @Transactional
    public void assignPatientToProvider(Patient patient, Person provider, RelationshipType relationshipType)
            throws ProviderDoesNotSupportRelationshipTypeException, PatientAlreadyAssignedToProviderException,
            PersonIsNotProviderException {
        try {
            assignPatientToProvider(patient, provider, relationshipType, new Date());
        }
        // we should never get a DateCannotBeInFuture exception since this method is suppose to do the assignment on the current date
        catch (DateCannotBeInFutureException e) {
            throw new APIException("DateCannotBeInFutureException should never be thrown here", e);
        }
    }

    @Override
    @Transactional
    public void unassignPatientFromProvider(Patient patient, Person provider, RelationshipType relationshipType,
            Date date) throws PatientNotAssignedToProviderException, PersonIsNotProviderException,
            InvalidRelationshipTypeException, DateCannotBeInFutureException {

        if (patient == null) {
            throw new APIException("Patient cannot be null");
        }

        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        if (relationshipType == null) {
            throw new APIException("Relationship type cannot be null");
        }

        if (!isProvider(provider)) {
            throw new PersonIsNotProviderException(provider + " is not a provider");
        }

        // we don't need to assure that the person supports the relationship type, but we need to make sure this a provider/patient relationship type
        if (!getAllProviderRoleRelationshipTypes(false).contains(relationshipType)) {
            throw new InvalidRelationshipTypeException("Invalid relationship type: " + relationshipType
                    + " is not a provider/patient relationship type");
        }

        // use current date if no date specified
        if (date == null) {
            date = new Date();
        }

        if (date.after(new Date())) {
            throw new DateCannotBeInFutureException("Unassignment date cannot be in the future");
        }

        // find the existing relationship
        List<Relationship> relationships = Context.getPersonService().getRelationships(provider, patient,
                relationshipType, date);
        if (relationships == null || relationships.size() == 0) {
            throw new PatientNotAssignedToProviderException(
                    patient.getPersonName() + " is not assigned to " + provider.getPersonName() + " with a "
                            + relationshipType + " relationship on " + Context.getDateFormat().format(date));
        }
        if (relationships.size() > 1) {
            throw new APIException("Duplicate " + relationshipType + " between " + provider.getPersonName()
                    + " and " + patient.getPersonName());
        }

        // go ahead and set the end date of the relationship
        Relationship relationship = relationships.get(0);
        relationship.setEndDate(ProviderManagementUtils.clearTimeComponent(date));
        Context.getPersonService().saveRelationship(relationship);
    }

    @Override
    @Transactional
    public void unassignPatientFromProvider(Patient patient, Person provider, RelationshipType relationshipType)
            throws PatientNotAssignedToProviderException, PersonIsNotProviderException,
            InvalidRelationshipTypeException {

        try {
            unassignPatientFromProvider(patient, provider, relationshipType, new Date());
        }
        // we should never get a DateCannotBeInFuture exception since this method is suppose to do the assignment on the current date
        catch (DateCannotBeInFutureException e) {
            throw new APIException("DateCannotBeInFutureException should never be thrown here", e);
        }
    }

    @Override
    @Transactional
    public void unassignAllPatientsFromProvider(Person provider, RelationshipType relationshipType)
            throws PersonIsNotProviderException, InvalidRelationshipTypeException {

        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        if (relationshipType == null) {
            throw new APIException("Relationship type cannot be null");
        }

        if (!isProvider(provider)) {
            throw new PersonIsNotProviderException(provider.getPersonName() + " is not a provider");
        }

        // we don't need to assure that the person supports the relationship type, but we need to make sure this a provider/patient relationship type
        if (!getAllProviderRoleRelationshipTypes(false).contains(relationshipType)) {
            throw new InvalidRelationshipTypeException("Invalid relationship type: " + relationshipType
                    + " is not a provider/patient relationship type");
        }

        // go ahead and end each relationship on the current date
        List<Relationship> relationships = Context.getPersonService().getRelationships(provider, null,
                relationshipType, ProviderManagementUtils.clearTimeComponent(new Date()));
        if (relationships != null || relationships.size() > 0) {
            for (Relationship relationship : relationships) {
                relationship.setEndDate(ProviderManagementUtils.clearTimeComponent(new Date()));
                Context.getPersonService().saveRelationship(relationship);
            }
        }
    }

    @Override
    @Transactional
    public void unassignAllPatientsFromProvider(Person provider) throws PersonIsNotProviderException {

        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        for (RelationshipType relationshipType : getAllProviderRoleRelationshipTypes(false)) {
            try {
                unassignAllPatientsFromProvider(provider, relationshipType);
            } catch (InvalidRelationshipTypeException e) {
                // we should never get this exception, since getAlProviderRoleRelationshipTypes
                // should only return valid relationship types; so if we do get this exception, throw a runtime exception
                // instead of forcing calling methods to catch it
                throw new APIException(e);
            }
        }
    }

    @Override
    @Transactional(readOnly = true)
    public List<Relationship> getPatientRelationshipsForProvider(Person provider, RelationshipType relationshipType,
            Date date) throws PersonIsNotProviderException, InvalidRelationshipTypeException {

        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        if (!isProvider(provider)) {
            throw new PersonIsNotProviderException(provider.getPersonName() + " is not a provider");
        }

        if (relationshipType != null && !getAllProviderRoleRelationshipTypes(false).contains(relationshipType)) {
            throw new InvalidRelationshipTypeException("Invalid relationship type: " + relationshipType
                    + " is not a provider/patient relationship type");
        }

        // get the specified relationships for the provider
        List<Relationship> relationships = Context.getPersonService().getRelationships(provider, null,
                relationshipType, date);

        // if a relationship type was not specified, we need to filter this list to only contain provider relationships
        if (relationshipType == null) {
            ProviderManagementUtils.filterNonProviderRelationships(relationships);
        }

        return relationships;
    }

    @Override
    @Transactional(readOnly = true)
    public List<Relationship> getPatientRelationshipsForProvider(Person provider, RelationshipType relationshipType)
            throws PersonIsNotProviderException, InvalidRelationshipTypeException {
        return getPatientRelationshipsForProvider(provider, relationshipType, null);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Patient> getPatientsOfProvider(Person provider, RelationshipType relationshipType, Date date)
            throws PersonIsNotProviderException, InvalidRelationshipTypeException {

        List<Relationship> relationships = Context.getService(ProviderManagementService.class)
                .getPatientRelationshipsForProvider(provider, relationshipType, date);

        // iterate through the relationships and fetch the patients
        Set<Patient> patients = new HashSet<Patient>();
        for (Relationship relationship : relationships) {

            if (!relationship.getPersonB().isPatient()) {
                throw new APIException("Invalid relationship " + relationship + ": person b must be a patient");
            }

            Patient p = Context.getPatientService().getPatient(relationship.getPersonB().getId());
            if (!p.isVoided()) {
                patients.add(p);
            }
        }

        return new ArrayList<Patient>(patients);
    }

    @Override
    public int getPatientsOfProviderCount(Person provider, RelationshipType relationshipType, Date date)
            throws PersonIsNotProviderException, InvalidRelationshipTypeException {

        // note that we can't just call getPatientsOfProvider and then do a count of the returns results,
        // because we want this method to be callable by users that don't have the right to view patients
        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        if (!isProvider(provider)) {
            throw new PersonIsNotProviderException(provider.getPersonName() + " is not a provider");
        }

        if (relationshipType != null && !getAllProviderRoleRelationshipTypes(false).contains(relationshipType)) {
            throw new InvalidRelationshipTypeException("Invalid relationship type: " + relationshipType
                    + " is not a provider/patient relationship type");
        }

        // get the specified relationships for the provider
        List<Relationship> relationships = Context.getPersonService().getRelationships(provider, null,
                relationshipType, date);

        // if a relationship type was not specified, we need to filter this list to only contain provider relationships
        if (relationshipType == null) {
            ProviderManagementUtils.filterNonProviderRelationships(relationships);
        }

        // TODO: the one flaw here is that this counts relationships with voided patients (but hopefully any relationships for voided patients will also be voided)
        return relationships != null ? relationships.size() : 0;
    }

    @Override
    @Transactional(readOnly = true)
    public List<Patient> getPatientsOfProvider(Person provider, RelationshipType relationshipType)
            throws PersonIsNotProviderException, InvalidRelationshipTypeException {
        return getPatientsOfProvider(provider, relationshipType, null);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Relationship> getProviderRelationshipsForPatient(Patient patient, Person provider,
            RelationshipType relationshipType, Date date)
            throws PersonIsNotProviderException, InvalidRelationshipTypeException {

        if (patient == null) {
            throw new APIException("Patient cannot be null");
        }

        if (provider != null && !isProvider(provider)) {
            throw new PersonIsNotProviderException(provider.getPersonName() + " is not a provider");
        }

        if (relationshipType != null && !getAllProviderRoleRelationshipTypes(false).contains(relationshipType)) {
            throw new InvalidRelationshipTypeException(
                    relationshipType + " is not a patient/provider relationship");
        }

        // fetch the relationships
        List<Relationship> relationships = Context.getPersonService().getRelationships(provider, patient,
                relationshipType, date);

        // if a relationship type was not specified, we need to filter this list to only contain provider relationships
        if (relationshipType == null) {
            ProviderManagementUtils.filterNonProviderRelationships(relationships);
        }

        return relationships;
    }

    @Override
    @Transactional(readOnly = true)
    public List<Relationship> getProviderRelationshipsForPatient(Patient patient, Person provider,
            RelationshipType relationshipType)
            throws PersonIsNotProviderException, InvalidRelationshipTypeException {
        return getProviderRelationshipsForPatient(patient, provider, relationshipType, null);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Person> getProvidersAsPersonsForPatient(Patient patient, RelationshipType relationshipType,
            Date date) throws InvalidRelationshipTypeException {

        List<Relationship> relationships;

        try {
            relationships = getProviderRelationshipsForPatient(patient, null, relationshipType, date);
        } catch (PersonIsNotProviderException e) {
            // should never reach here since we aren't specifying a provider in the above method
            // just through an APIException here to avoid having have this method throw this exception
            throw new APIException(e);
        }

        Set<Person> providers = new HashSet<Person>();

        for (Relationship relationship : relationships) {
            if (!isProvider(relationship.getPersonA())) {
                // something has gone really wrong here
                throw new APIException(relationship.getPersonA().getPersonName() + " is not a provider");
            } else {
                providers.add(relationship.getPersonA());
            }
        }

        return new ArrayList<Person>(providers);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Person> getProvidersAsPersonsForPatient(Patient patient, RelationshipType relationshipType)
            throws InvalidRelationshipTypeException {
        return getProvidersAsPersonsForPatient(patient, relationshipType, null);
    }

    @Override
    @Transactional
    public void transferPatients(List<Patient> patients, Person sourceProvider, Person destinationProvider,
            RelationshipType relationshipType, Date date) throws ProviderDoesNotSupportRelationshipTypeException,
            SourceProviderSameAsDestinationProviderException, PersonIsNotProviderException,
            InvalidRelationshipTypeException, PatientNotAssignedToProviderException, DateCannotBeInFutureException {

        if (sourceProvider == null) {
            throw new APIException("Source provider cannot be null");
        }

        if (destinationProvider == null) {
            throw new APIException("Destination provider cannot be null");
        }

        if (!isProvider(sourceProvider)) {
            throw new PersonIsNotProviderException(sourceProvider.getPersonName() + " is not a provider");
        }

        if (!isProvider(destinationProvider)) {
            throw new PersonIsNotProviderException(destinationProvider.getPersonName() + " is not a provider");
        }

        if (sourceProvider.equals(destinationProvider)) {
            throw new SourceProviderSameAsDestinationProviderException("Provider " + sourceProvider.getPersonName()
                    + " is the same as provider " + destinationProvider.getPersonName());
        }

        if (relationshipType == null) {
            throw new APIException("Relationship type cannot be null");
        }

        // assign these patients to the new provider, unassign them from the old provider
        for (Patient patient : patients) {
            try {
                assignPatientToProvider(patient, destinationProvider, relationshipType, date);
            } catch (PatientAlreadyAssignedToProviderException e) {
                // we can ignore this exception; no need to assign patient if already assigned
            }

            unassignPatientFromProvider(patient, sourceProvider, relationshipType, date);
        }
    }

    @Override
    public void transferPatients(List<Patient> patients, Person sourceProvider, Person destinationProvider,
            RelationshipType relationshipType) throws ProviderDoesNotSupportRelationshipTypeException,
            SourceProviderSameAsDestinationProviderException, PersonIsNotProviderException,
            InvalidRelationshipTypeException, PatientNotAssignedToProviderException, DateCannotBeInFutureException {

        transferPatients(patients, sourceProvider, destinationProvider, relationshipType, new Date());
    }

    @Override
    @Transactional
    public void transferAllPatients(Person sourceProvider, Person destinationProvider,
            RelationshipType relationshipType, Date date) throws ProviderDoesNotSupportRelationshipTypeException,
            SourceProviderSameAsDestinationProviderException, PersonIsNotProviderException,
            InvalidRelationshipTypeException, DateCannotBeInFutureException {

        try {
            transferPatients(getPatientsOfProvider(sourceProvider, relationshipType, date), sourceProvider,
                    destinationProvider, relationshipType, date);
        } catch (PatientNotAssignedToProviderException e) {
            // we should fail hard here, because getPatientsOfProvider should only return patients of the provider,
            // so if this exception has been thrown, something has gone really wrong
            throw new APIException("All patients here should be assigned to provider,", e);
        }
    }

    @Override
    @Transactional
    public void transferAllPatients(Person sourceProvider, Person destinationProvider,
            RelationshipType relationshipType) throws ProviderDoesNotSupportRelationshipTypeException,
            SourceProviderSameAsDestinationProviderException, PersonIsNotProviderException,
            InvalidRelationshipTypeException, DateCannotBeInFutureException {

        transferAllPatients(sourceProvider, destinationProvider, relationshipType, new Date());
    }

    @Override
    @Transactional
    public void transferAllPatients(Person sourceProvider, Person destinationProvider, Date date)
            throws ProviderDoesNotSupportRelationshipTypeException, PersonIsNotProviderException,
            SourceProviderSameAsDestinationProviderException, DateCannotBeInFutureException {
        for (RelationshipType relationshipType : getAllProviderRoleRelationshipTypes(false)) {
            try {
                transferAllPatients(sourceProvider, destinationProvider, relationshipType, date);
            } catch (InvalidRelationshipTypeException e) {
                // we should never get this exception, since getAlProviderRoleRelationshipTypes
                // should only return valid relationship types; so if we do get this exception, throw a runtime exception
                // instead of forcing calling methods to catch it
                throw new APIException(e);
            }
        }
    }

    @Override
    @Transactional
    public void transferAllPatients(Person sourceProvider, Person destinationProvider)
            throws ProviderDoesNotSupportRelationshipTypeException, PersonIsNotProviderException,
            SourceProviderSameAsDestinationProviderException, DateCannotBeInFutureException {

        transferAllPatients(sourceProvider, destinationProvider, new Date());
    }

    @Override
    @Transactional(readOnly = true)
    public RelationshipType getSupervisorRelationshipType() {

        if (supervisorRelationshipType == null) {
            supervisorRelationshipType = Context.getPersonService()
                    .getRelationshipTypeByUuid(ProviderManagementConstants.SUPERVISOR_RELATIONSHIP_TYPE_UUID);

            // if the relationship type is still null, throw an exception here
            if (supervisorRelationshipType == null) {
                throw new APIException("Superviser relationship type does not exist in relationship type table");
            }
        }

        return supervisorRelationshipType;
    }

    @Override
    @Transactional
    public void assignProviderToSupervisor(Person provider, Person supervisor, Date date)
            throws PersonIsNotProviderException, InvalidSupervisorException,
            ProviderAlreadyAssignedToSupervisorException, DateCannotBeInFutureException {

        if (supervisor == null) {
            throw new APIException("Supervisor cannot be null");
        }

        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        if (!isProvider(supervisor)) {
            throw new PersonIsNotProviderException(supervisor.getPersonName() + " is not a provider");
        }

        if (!isProvider(provider)) {
            throw new PersonIsNotProviderException(provider.getPersonName() + " is not a provider");
        }

        if (!canSupervise(supervisor, provider)) {
            throw new InvalidSupervisorException(
                    supervisor.getPersonName() + " is not a valid supervisor for " + provider.getPersonName());
        }

        // if no date specified, use today's date
        if (date == null) {
            date = new Date();
        }

        if (date.after(new Date())) {
            throw new DateCannotBeInFutureException("Unassignment date cannot be in the future");
        }

        // test to mark sure the relationship doesn't already exist
        List<Relationship> relationships = Context.getPersonService().getRelationships(supervisor, provider,
                getSupervisorRelationshipType(), date);
        if (relationships != null && relationships.size() > 0) {
            throw new ProviderAlreadyAssignedToSupervisorException(
                    provider.getPersonName() + " is already assigned to " + supervisor.getPersonName());
        }

        // go ahead and create the relationship
        Relationship relationship = new Relationship();
        relationship.setPersonA(supervisor);
        relationship.setPersonB(provider);
        relationship.setRelationshipType(getSupervisorRelationshipType());
        relationship.setStartDate(ProviderManagementUtils.clearTimeComponent(date));
        Context.getPersonService().saveRelationship(relationship);
    }

    @Override
    @Transactional
    public void assignProviderToSupervisor(Person provider, Person supervisor) throws PersonIsNotProviderException,
            InvalidSupervisorException, ProviderAlreadyAssignedToSupervisorException {

        try {
            assignProviderToSupervisor(provider, supervisor, new Date());
        }
        // we should never get a DateCannotBeInFuture exception since this method is suppose to do the assignment on the current date
        catch (DateCannotBeInFutureException e) {
            throw new APIException("DateCannotBeInFutureException should never be thrown here", e);
        }
    }

    @Override
    @Transactional
    public void unassignProviderFromSupervisor(Person provider, Person supervisor, Date date)
            throws PersonIsNotProviderException, ProviderNotAssignedToSupervisorException,
            DateCannotBeInFutureException {

        if (supervisor == null) {
            throw new APIException("Supervisor cannot be null");
        }

        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        if (!isProvider(supervisor)) {
            throw new PersonIsNotProviderException(supervisor.getPersonName() + " is not a provider");
        }

        if (!isProvider(provider)) {
            throw new PersonIsNotProviderException(provider.getPersonName() + " is not a provider");
        }

        // if no date specified, use today's date
        if (date == null) {
            date = new Date();
        }

        if (date.after(new Date())) {
            throw new DateCannotBeInFutureException("Unassignment date cannot be in the future");
        }

        // find the existing relationship
        List<Relationship> relationships = Context.getPersonService().getRelationships(supervisor, provider,
                getSupervisorRelationshipType(), date);
        if (relationships == null || relationships.size() == 0) {
            throw new ProviderNotAssignedToSupervisorException(
                    "Provider " + provider.getPersonName() + " is not assigned to supervisor "
                            + supervisor.getPersonName() + " on " + Context.getDateFormat().format(date));
        }
        if (relationships.size() > 1) {
            throw new APIException("Duplicate supervisor relationship between " + provider + " and " + supervisor);
        }

        // go ahead and set the end date of the relationship
        Relationship relationship = relationships.get(0);
        relationship.setEndDate(ProviderManagementUtils.clearTimeComponent(date));
        Context.getPersonService().saveRelationship(relationship);
    }

    @Override
    @Transactional
    public void unassignProviderFromSupervisor(Person provider, Person supervisor)
            throws PersonIsNotProviderException, ProviderNotAssignedToSupervisorException {

        try {
            unassignProviderFromSupervisor(provider, supervisor, new Date());
        }
        // we should never get a DateCannotBeInFuture exception since this method is suppose to do the assignment on the current date
        catch (DateCannotBeInFutureException e) {
            throw new APIException("DateCannotBeInFutureException should never be thrown here", e);
        }
    }

    @Override
    @Transactional
    public void unassignAllSupervisorsFromProvider(Person provider) throws PersonIsNotProviderException {
        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        if (!isProvider(provider)) {
            throw new PersonIsNotProviderException(provider.getPersonName() + " is not a provider");
        }

        // go ahead and end each relationship on the current date
        List<Relationship> relationships = Context.getPersonService().getRelationships(null, provider,
                getSupervisorRelationshipType(), ProviderManagementUtils.clearTimeComponent(new Date()));
        if (relationships != null || relationships.size() > 0) {
            for (Relationship relationship : relationships) {
                relationship.setEndDate(ProviderManagementUtils.clearTimeComponent(new Date()));
                Context.getPersonService().saveRelationship(relationship);
            }
        }
    }

    @Override
    @Transactional
    public void unassignAllProvidersFromSupervisor(Person supervisor) throws PersonIsNotProviderException {
        if (supervisor == null) {
            throw new APIException("Provider cannot be null");
        }

        if (!isProvider(supervisor)) {
            throw new PersonIsNotProviderException(supervisor.getPersonName() + " is not a provider");
        }

        // go ahead and end each relationship on the current date
        List<Relationship> relationships = Context.getPersonService().getRelationships(supervisor, null,
                getSupervisorRelationshipType(), ProviderManagementUtils.clearTimeComponent(new Date()));
        if (relationships != null || relationships.size() > 0) {
            for (Relationship relationship : relationships) {
                relationship.setEndDate(ProviderManagementUtils.clearTimeComponent(new Date()));
                Context.getPersonService().saveRelationship(relationship);
            }
        }
    }

    @Override
    @Transactional(readOnly = true)
    public List<Relationship> getSupervisorRelationshipsForProvider(Person provider, Date date)
            throws PersonIsNotProviderException {

        if (provider == null) {
            throw new APIException("Provider cannot be null");
        }

        if (!isProvider(provider)) {
            throw new PersonIsNotProviderException(provider.getPersonName() + " is not a provider");
        }

        return Context.getPersonService().getRelationships(null, provider, getSupervisorRelationshipType(), date);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Relationship> getSupervisorRelationshipsForProvider(Person provider)
            throws PersonIsNotProviderException {
        return getSupervisorRelationshipsForProvider(provider, null);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Person> getSupervisorsForProvider(Person provider, Date date) throws PersonIsNotProviderException {

        List<Relationship> relationships = getSupervisorRelationshipsForProvider(provider, date);

        Set<Person> providers = new HashSet<Person>();

        for (Relationship relationship : relationships) {
            if (!isProvider(relationship.getPersonA())) {
                // something has gone really wrong here
                throw new APIException(relationship.getPersonA().getPersonName() + " is not a provider");
            } else {
                providers.add(relationship.getPersonA());
            }
        }

        return new ArrayList<Person>(providers);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Person> getSupervisorsForProvider(Person provider) throws PersonIsNotProviderException {
        return getSupervisorsForProvider(provider, null);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Relationship> getSuperviseeRelationshipsForSupervisor(Person supervisor, Date date)
            throws PersonIsNotProviderException {

        if (supervisor == null) {
            throw new APIException("Supervisor cannot be null");
        }

        if (!isProvider(supervisor)) {
            throw new PersonIsNotProviderException(supervisor.getPersonName() + " is not a provider");
        }

        return Context.getPersonService().getRelationships(supervisor, null, getSupervisorRelationshipType(), date);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Relationship> getSuperviseeRelationshipsForSupervisor(Person supervisor)
            throws PersonIsNotProviderException {
        return getSuperviseeRelationshipsForSupervisor(supervisor, null);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Person> getSuperviseesForSupervisor(Person supervisor, Date date)
            throws PersonIsNotProviderException {

        List<Relationship> relationships = getSuperviseeRelationshipsForSupervisor(supervisor, date);

        Set<Person> providers = new HashSet<Person>();

        for (Relationship relationship : relationships) {
            if (!isProvider(relationship.getPersonA())) {
                // something has gone really wrong here
                throw new APIException(relationship.getPersonA().getPersonName() + " is not a provider");
            } else {
                providers.add(relationship.getPersonB());
            }
        }

        return new ArrayList<Person>(providers);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Person> getSuperviseesForSupervisor(Person supervisor) throws PersonIsNotProviderException {
        return getSuperviseesForSupervisor(supervisor, null);
    }

    @Override
    @Transactional
    public void transferSupervisees(List<Person> supervisees, Person sourceSupervisor, Person destinationSupervisor,
            Date date) throws PersonIsNotProviderException, SourceProviderSameAsDestinationProviderException,
            InvalidSupervisorException, ProviderNotAssignedToSupervisorException, DateCannotBeInFutureException {

        if (sourceSupervisor == null) {
            throw new APIException("Source supervisor cannot be null");
        }

        if (destinationSupervisor == null) {
            throw new APIException("Destination supervisor cannot be null");
        }

        if (!isProvider(sourceSupervisor)) {
            throw new PersonIsNotProviderException(sourceSupervisor.getPersonName() + " is not a provider");
        }

        if (!isProvider(destinationSupervisor)) {
            throw new PersonIsNotProviderException(destinationSupervisor.getPersonName() + " is not a provider");
        }

        if (sourceSupervisor.equals(destinationSupervisor)) {
            throw new SourceProviderSameAsDestinationProviderException(
                    "Provider " + sourceSupervisor.getPersonName() + " is the same as provider "
                            + destinationSupervisor.getPersonName());
        }

        for (Person supervisee : supervisees) {
            // first assign the supervisee to the new superviser
            try {
                assignProviderToSupervisor(supervisee, destinationSupervisor, date);
            } catch (ProviderAlreadyAssignedToSupervisorException e) {
                // don't worry about doing anything here, no need to worry about assigning if already assigned
                // however, note that we don't trap the invalid supervisor exception that could occur here
            }

            // now unassign the supervisee from the old supervisor
            unassignProviderFromSupervisor(supervisee, sourceSupervisor, date);
        }
    }

    @Override
    @Transactional
    public void transferSupervisees(List<Person> supervisees, Person sourceSupervisor, Person destinationSupervisor)
            throws PersonIsNotProviderException, SourceProviderSameAsDestinationProviderException,
            InvalidSupervisorException, ProviderNotAssignedToSupervisorException, DateCannotBeInFutureException {

        transferSupervisees(supervisees, sourceSupervisor, destinationSupervisor, new Date());
    }

    @Override
    public void transferAllSupervisees(Person sourceSupervisor, Person destinationSupervisor, Date date)
            throws PersonIsNotProviderException, SourceProviderSameAsDestinationProviderException,
            InvalidSupervisorException, DateCannotBeInFutureException {
        try {
            transferSupervisees(getSuperviseesForSupervisor(sourceSupervisor, date), sourceSupervisor,
                    destinationSupervisor, date);
        } catch (ProviderNotAssignedToSupervisorException e) {
            // we can fail hard here because getSuperviseesForSupervisor should never return providers who aren't supervisees of sourceSupervisor
            throw new RuntimeException(e);
        }
    }

    @Override
    public void transferAllSupervisees(Person sourceSupervisor, Person destinationSupervisor)
            throws PersonIsNotProviderException, SourceProviderSameAsDestinationProviderException,
            InvalidSupervisorException, DateCannotBeInFutureException {

        transferAllSupervisees(sourceSupervisor, destinationSupervisor, new Date());
    }

    /**
     * Methods to fetch Provider objects based on persons
     */

    @Override
    @Transactional(readOnly = true)
    public List<Provider> getProvidersByPerson(Person person, boolean includeRetired) {
        return dao.getProvidersByPerson(person, includeRetired);
    }

    /**
     * Utility methods
     */
    private List<Person> providersToPersons(List<Provider> providers) {

        if (providers == null) {
            return null;
        }

        Set<Person> persons = new HashSet<Person>();

        // note that simply ignores providers that are not person, as the module cannot handle them (and I believe that it has been determined that OpemMRS won't support them)
        for (Provider provider : providers) {
            if (provider.getPerson() != null) {
                persons.add(provider.getPerson());
            } else {
                log.warn("Ignoring provider " + provider.getId() + " because they are not a person");
            }
        }

        return new ArrayList<Person>(persons);
    }

    private boolean supportsRelationshipType(Provider provider, RelationshipType relationshipType) {

        if (provider == null) {
            throw new APIException("Provider should not be null");
        }

        if (relationshipType == null) {
            throw new APIException("Relationship type should not be null");
        }

        // if this provider has no role, return false
        if (provider.getProviderRole() == null) {
            return false;
        }
        // otherwise, test if the provider's role supports the specified relationship type
        else {
            return provider.getProviderRole().supportsRelationshipType(relationshipType);
        }
    }
}