/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * 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. * * The Original Code is part of dcm4che, an implementation of DICOM(TM) in * Java(TM), hosted at https://github.com/gunterze/dcm4che. * * The Initial Developer of the Original Code is * Agfa Healthcare. * Portions created by the Initial Developer are Copyright (C) 2011-2014 * the Initial Developer. All Rights Reserved. * * Contributor(s): * See @authors listed below * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package org.dcm4chee.archive.patient.impl; import java.util.*; import javax.ejb.Stateless; import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.dcm4che3.data.Attributes; import org.dcm4che3.data.IDWithIssuer; import org.dcm4che3.data.PersonName; import org.dcm4che3.data.Tag; import org.dcm4che3.util.UIDUtils; import org.dcm4chee.archive.conf.AttributeFilter; import org.dcm4chee.archive.conf.Entity; import org.dcm4chee.archive.conf.MetadataUpdateStrategy; import org.dcm4chee.archive.conf.StoreParam; import org.dcm4chee.archive.entity.*; import org.dcm4chee.archive.issuer.IssuerService; import org.dcm4chee.archive.patient.NonUniquePatientException; import org.dcm4chee.archive.patient.PatientCircularMergedException; import org.dcm4chee.archive.patient.PatientMergedException; import org.dcm4chee.archive.patient.PatientSelector; import org.dcm4chee.archive.patient.PatientSelectorFactory; import org.dcm4chee.archive.patient.PatientService; import org.dcm4chee.archive.util.ArchiveDeidentifier; import org.hibernate.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mysema.query.BooleanBuilder; import com.mysema.query.jpa.hibernate.HibernateQuery; import com.mysema.query.jpa.hibernate.HibernateSubQuery; import com.mysema.query.types.ExpressionUtils; import com.mysema.query.types.Predicate; import com.mysema.query.types.expr.BooleanExpression; /** * @author Gunter Zeilinger <gunterze@gmail.com> * */ @Stateless public class PatientServiceEJB implements PatientService { private static Logger LOG = LoggerFactory .getLogger(PatientServiceEJB.class); @PersistenceContext(name = "dcm4chee-arc", unitName = "dcm4chee-arc") private EntityManager em; @Inject private IssuerService issuerService; @Override public Patient updateOrCreatePatientOnCStore(Attributes attrs, PatientSelector selector, StoreParam storeParam) throws PatientCircularMergedException { return updateOrCreatePatientByDICOM(attrs, selector, storeParam); } @Override public Patient updateOrCreatePatientOnMPPSNCreate(Attributes attrs, PatientSelector selector, StoreParam storeParam) throws PatientCircularMergedException { return updateOrCreatePatientByDICOM(attrs, selector, storeParam); } private Patient updateOrCreatePatientByDICOM(Attributes attrs, PatientSelector selector, StoreParam storeParam) throws PatientCircularMergedException { Set<IDWithIssuer> pids = IDWithIssuer.pidsOf(attrs); Patient patient = null; try { patient = findPatientByDICOM(pids, attrs, selector); } catch (NonUniquePatientException e) { LOG.info("Could not associate unique Patient Record to " + "received DICOM object - create new Patient Record:", e); } if (patient == null) return createPatient(pids, attrs, storeParam); patient = followMergedWith(patient); updatePatientByDICOM(patient, attrs, storeParam, pids); return patient; } private Patient findPatientByDICOM(Collection<IDWithIssuer> pids, Attributes attrs, PatientSelector selector) throws NonUniquePatientException { List<Patient> candidates; if (!pids.isEmpty()) { candidates = findPatientByIDs(pids); } else { String familyName = new PersonName(attrs.getString(Tag.PatientName)) .get(PersonName.Component.FamilyName); if (familyName != null) candidates = findPatientByFamilyName(familyName); else throw new NonUniquePatientException( "No Patient ID and no Patient Family Name"); } return selector.select(candidates, attrs, pids); } private List<Patient> findPatientByFamilyName(String familyName) { return em .createNamedQuery(Patient.FIND_BY_PATIENT_FAMILY_NAME, Patient.class).setParameter(1, familyName) .getResultList(); } private List<Patient> findPatientByIDs(Collection<IDWithIssuer> pids) { BooleanBuilder builder = new BooleanBuilder(); Collection<BooleanExpression> eqIDs = new ArrayList<BooleanExpression>( pids.size()); for (IDWithIssuer pid : pids) { BooleanExpression eqID = QPatientID.patientID.id.eq(pid.getID()); if (pid.getIssuer() == null) { builder.or(eqID); } else { builder.or(ExpressionUtils.and(eqID, eqOrNoIssuer(pid.getIssuer()))); eqIDs.add(eqID); } } HibernateSubQuery matchingIDs = new HibernateSubQuery() .from(QPatientID.patientID) .leftJoin(QPatientID.patientID.issuer, QIssuer.issuer) .where(builder); Session session = em.unwrap(Session.class); return new HibernateQuery(session) .from(QPatient.patient) .leftJoin(QPatient.patient.attributesBlob, QAttributesBlob.attributesBlob) .fetch() .where(QPatient.patient.pk.in(matchingIDs .list(QPatientID.patientID.patient.pk))) .list(QPatient.patient); } private Predicate eqOrNoIssuer(org.dcm4che3.data.Issuer issuer) { String id = issuer.getLocalNamespaceEntityID(); String uid = issuer.getUniversalEntityID(); String uidType = issuer.getUniversalEntityIDType(); BooleanBuilder builder = new BooleanBuilder(); if (id != null) builder.and(eqOrNoLocalNamespaceEntityID(id)); if (uid != null) builder.and(eqOrNoUniversalEntityID(uid, uidType)); return builder; } private Predicate eqOrNoLocalNamespaceEntityID(String id) { return ExpressionUtils.or(QIssuer.issuer.localNamespaceEntityID.eq(id), QIssuer.issuer.localNamespaceEntityID.isNull()); } private Predicate eqOrNoUniversalEntityID(String uid, String uidType) { return ExpressionUtils.or(ExpressionUtils.and( QIssuer.issuer.universalEntityID.eq(uid), QIssuer.issuer.universalEntityIDType.eq(uidType)), QIssuer.issuer.universalEntityID.isNull()); } private Patient followMergedWith(Patient patient) throws PatientCircularMergedException { Patient mergedWith = patient.getMergedWith(); if (mergedWith == null) return patient; HashSet<Long> pks = new HashSet<Long>(); pks.add(patient.getPk()); do { if (!pks.add(mergedWith.getPk())) { throw new PatientCircularMergedException(patient); } patient = mergedWith; mergedWith = patient.getMergedWith(); } while (mergedWith != null); return patient; } private Patient createPatient(Set<IDWithIssuer> pids, Attributes attrs, StoreParam storeParam) { Patient patient = new Patient(); patient.setAttributes(attrs, storeParam.getAttributeFilter(Entity.Patient), storeParam.getFuzzyStr(), storeParam.getNullValueForQueryFields()); if (pids.isEmpty() && storeParam.getIssuerOfPatientID()!=null) { pids= new HashSet<IDWithIssuer>(); pids.add(new IDWithIssuer(UIDUtils.createUID(),storeParam.getIssuerOfPatientID())); } patient.setNoPatientID(pids.isEmpty()); patient.setPatientIDs(createPatientIDs(pids, patient, storeParam.isDeIdentifyLogs())); em.persist(patient); LOG.info("Create {}", patient.toString(storeParam.isDeIdentifyLogs())); return patient; } private Collection<PatientID> createPatientIDs( Collection<IDWithIssuer> pids, Patient patient, boolean deidentify) { Collection<PatientID> patientIDs = new ArrayList<PatientID>(pids.size()); for (IDWithIssuer pid : pids) patientIDs.add(createPatientID(pid, patient, deidentify)); return patientIDs; } private PatientID createPatientID(IDWithIssuer pid, Patient patient, boolean deidentify) { PatientID patientID = new PatientID(); patientID.setID(pid.getID()); patientID.setIdentifierTypeCode(pid.getIdentifierTypeCode()); patientID.setIssuer(findOrCreateIssuer(pid.getIssuer())); patientID.setPatient(patient); LOG.info("Add {} to {}", patientID, patient.toString(deidentify)); return patientID; } private Issuer findOrCreateIssuer(org.dcm4che3.data.Issuer issuer) { if (issuer == null) return null; return issuerService.findOrCreate(new Issuer(issuer)); } @Override public void updatePatientByCStore(Patient patient, Attributes attrs, StoreParam storeParam) { updatePatientByDICOM(patient, attrs, storeParam, IDWithIssuer.pidsOf(attrs)); } private void updatePatientByDICOM(Patient patient, Attributes attrs, StoreParam storeParam, Collection<IDWithIssuer> pids) { if (mergePatientIDs(patient, pids, storeParam.isDeIdentifyLogs())) { patient.updateOtherPatientIDs(); } Attributes patientAttrs = patient.getAttributes(); AttributeFilter filter = storeParam.getAttributeFilter(Entity.Patient); Attributes modified = new Attributes(); boolean deident = storeParam.isDeIdentifyLogs(); if (Utils.updateAttributes(patientAttrs, attrs, modified , filter, MetadataUpdateStrategy.COERCE_MERGE)) { LOG.info("[{}]: Update {}:\n{}\nmodified:\n{}", this.getClass().getName(), patient.toString(deident), deident ? patientAttrs.toString(ArchiveDeidentifier.DEFAULT) : patientAttrs, deident ? modified.toString(ArchiveDeidentifier.DEFAULT) : modified); patient.setAttributes(patientAttrs, filter, storeParam.getFuzzyStr(), storeParam.getNullValueForQueryFields()); } } private boolean mergePatientIDs(Patient patient, Collection<IDWithIssuer> pids, boolean deidentify) { boolean modified = false; Collection<PatientID> patientIDs = patient.getPatientIDs(); Collection<IDWithIssuer> add = new ArrayList<IDWithIssuer>(pids); for (Iterator<IDWithIssuer> iter = add.iterator(); iter.hasNext();) { IDWithIssuer pid = iter.next(); PatientID patientID = selectFrom(patientIDs, pid); if (patientID == null) continue; iter.remove(); if (pid.getIssuer() == null) continue; Issuer issuer = patientID.getIssuer(); if (issuer == null) { patientID.setIssuer(findOrCreateIssuer(pid.getIssuer())); modified = true; LOG.info("Set Issuer of {} of Patient {}", patientID, patient.toString(deidentify)); } else if (issuer.merge(pid.getIssuer())) { modified = true; LOG.info("Updated Issuer of {} of Patient {}", patientID, patient.toString(deidentify)); } } if (add.size() == pids.size()) { // no matching pid LOG.info("Patient IDs of exisiting {} does not match any Patient ID in received DICOM object - ignore Patient IDs in received object"); return false; } for (IDWithIssuer pid : add) patientIDs.add(createPatientID(pid, patient, deidentify)); patient.setNoPatientID(patientIDs.isEmpty()); return modified || !add.isEmpty(); } private PatientID selectFrom(Collection<PatientID> patientIDs, IDWithIssuer pid) { for (PatientID patientID : patientIDs) { if (pid.getID().equals(patientID.getID()) && (pid.getIssuer() == null || pid.getIssuer().matches(patientID.getIssuer()))) return patientID; } return null; } private void updatePatientByHL7(Patient patient, Attributes attrs, Collection<IDWithIssuer> pids, StoreParam storeParam) { if (mergePatientIDs(patient, pids, storeParam.isDeIdentifyLogs())) { patient.updateOtherPatientIDs(); } Attributes patientAttrs = patient.getAttributes(); AttributeFilter filter = storeParam.getAttributeFilter(Entity.Patient); if (Utils.updateAttributes(patientAttrs, attrs, null, filter, MetadataUpdateStrategy.COERCE_MERGE)) patient.setAttributes(patientAttrs, filter, storeParam.getFuzzyStr(), storeParam.getNullValueForQueryFields()); } @Override public Patient updateOrCreatePatientByHL7(Attributes attrs, StoreParam storeParam) throws NonUniquePatientException, PatientMergedException { // TODO make PatientSelector configurable PatientSelector selector = PatientSelectorFactory .createSelector(storeParam); Set<IDWithIssuer> pids = IDWithIssuer.pidsOf(attrs); return updateOrCreatePatientByHL7(attrs, storeParam, selector, pids); } private Patient updateOrCreatePatientByHL7(Attributes attrs, StoreParam storeParam, PatientSelector selector, Set<IDWithIssuer> pids) throws NonUniquePatientException, PatientMergedException { Patient patient = selector.select(findPatientByIDs(pids), attrs, pids); if (patient == null) return createPatient(pids, attrs, storeParam); if (patient.getMergedWith() != null) throw new PatientMergedException(patient); updatePatientByHL7(patient, attrs, pids, storeParam); return patient; } @Override public void mergePatientByHL7(Attributes attrs, Attributes priorAttrs, StoreParam storeParam) throws NonUniquePatientException, PatientMergedException { PatientSelector selector = PatientSelectorFactory .createSelector(storeParam); Set<IDWithIssuer> pids = IDWithIssuer.pidsOf(attrs); Set<IDWithIssuer> priorPIDs = IDWithIssuer.pidsOf(priorAttrs); Patient prior = updateOrCreatePatientByHL7(priorAttrs, storeParam, selector, priorPIDs); Patient pat = updateOrCreatePatientByHL7(attrs, storeParam, selector, pids); mergePatient(pat, prior, priorPIDs, storeParam.isDeIdentifyLogs()); } private void mergePatient(Patient pat, Patient prior, Collection<IDWithIssuer> priorPIDs, boolean deidentify) { if (pat == prior) throw new IllegalArgumentException("Cannot merge " + pat + " with itself"); LOG.info("Merge {} with {}", prior, pat); moveStudies(pat, prior); moveModalityWorklistItems(pat, prior); moveModalityPerformedProcedureSteps(pat, prior); boolean movePatientIDs = movePatientIDs(pat, prior, priorPIDs); Collection<Patient> linkedPatients = prior.getLinkedPatients(); if (movePatientIDs || !linkedPatients.isEmpty()) { for (Patient linked : linkedPatients) { unlinkPatientIDs(prior, linked, deidentify); unlinkPatientIDs(linked, prior, deidentify); linkPatientIDs(pat, linked, deidentify); linkPatientIDs(linked, pat, deidentify); linked.updateOtherPatientIDs(); } prior.updateOtherPatientIDs(); pat.updateOtherPatientIDs(); } prior.setMergedWith(pat); } @Override public void linkPatient(Attributes attrs, Attributes otherAttrs, StoreParam storeParam) throws NonUniquePatientException, PatientMergedException { Patient pat = updateOrCreatePatientByHL7(attrs, storeParam); Patient other = updateOrCreatePatientByHL7(otherAttrs, storeParam); linkPatient(pat, other, storeParam.isDeIdentifyLogs()); } private void linkPatient(Patient pat, Patient other, boolean deidentify) { if (pat == other) throw new IllegalArgumentException("Cannot link " + pat + " with itself"); LOG.info("Link {} with {}", other, pat); linkPatientIDs(pat, other, deidentify); linkPatientIDs(other, pat, deidentify); pat.updateOtherPatientIDs(); other.updateOtherPatientIDs(); } private void linkPatientIDs(Patient pat, Patient other, boolean deidentify) { Collection<PatientID> linkedPatientIDs = pat.getLinkedPatientIDs(); if (linkedPatientIDs == null) { linkedPatientIDs = new ArrayList<PatientID>(); pat.setLinkedPatientIDs(linkedPatientIDs); } for (PatientID pid : other.getPatientIDs()) { if (!contains(linkedPatientIDs, pid)) { linkedPatientIDs.add(pid); LOG.info("Link {} of {} to {}", pid, other.toString(deidentify), pat.toString(deidentify)); } } } private boolean contains(Collection<PatientID> pids, PatientID other) { long pk = other.getPk(); for (PatientID pid : pids) if (pid.getPk() == pk) return true; return false; } @Override public void unlinkPatient(Attributes attrs, Attributes otherAttrs, StoreParam storeParam) throws NonUniquePatientException, PatientMergedException { // TODO make PatientSelector configurable PatientSelector selector = PatientSelectorFactory .createSelector(storeParam); Collection<IDWithIssuer> pids = IDWithIssuer.pidsOf(attrs); Patient pat = selector.select(findPatientByIDs(pids), attrs, pids); if (pat == null) return; if (pat.getMergedWith() != null) throw new PatientMergedException(pat); Collection<IDWithIssuer> otherPIDs = IDWithIssuer.pidsOf(otherAttrs); Patient other = selector.select(findPatientByIDs(otherPIDs), attrs, otherPIDs); if (other == null) return; if (other.getMergedWith() != null) throw new PatientMergedException(other); if (pat == other) throw new IllegalArgumentException("Cannot link " + pat + " with itself"); LOG.info("Unlink {} from {}", other, pat); mergePatientIDs(pat, pids, storeParam.isDeIdentifyLogs()); mergePatientIDs(other, otherPIDs, storeParam.isDeIdentifyLogs()); unlinkPatientIDs(pat, other, storeParam.isDeIdentifyLogs()); unlinkPatientIDs(other, pat, storeParam.isDeIdentifyLogs()); pat.updateOtherPatientIDs(); other.updateOtherPatientIDs(); } private void unlinkPatientIDs(Patient pat, Patient other, boolean deidentify) { Collection<PatientID> linkedPatientIDs = pat.getLinkedPatientIDs(); for (PatientID pid : other.getPatientIDs()) { if (removePatientID(linkedPatientIDs, pid)) { LOG.info("Unlink {} of {} from {}", pid, other.toString(deidentify), pat.toString(deidentify)); } } } private boolean removePatientID(Collection<PatientID> pids, PatientID other) { long pk = other.getPk(); for (Iterator<PatientID> iter = pids.iterator(); iter.hasNext();) { PatientID pid = iter.next(); if (pid.getPk() == pk) { iter.remove(); return true; } } return false; } private void moveStudies(Patient pat, Patient prior) { Collection<Study> studies = (pat.getStudies() != null ? pat .getStudies() : new ArrayList<Study>()); for (Iterator<Study> iter = (prior.getStudies() != null ? prior .getStudies().iterator() : new ArrayList<Study>().iterator()); iter .hasNext();) { Study study = iter.next(); iter.remove(); study.setPatient(pat); studies.add(study); LOG.info("Move {} from {} to {}", study, prior, pat); } } private void moveModalityWorklistItems(Patient pat, Patient prior) { Collection<MWLItem> mwlItems = (pat.getModalityWorklistItems() != null ? pat .getModalityWorklistItems() : new ArrayList<MWLItem>()); for (Iterator<MWLItem> iter = (prior.getModalityWorklistItems() != null ? prior .getModalityWorklistItems().iterator() : new ArrayList<MWLItem>().iterator()); iter.hasNext();) { MWLItem mwlItem = iter.next(); iter.remove(); mwlItem.setPatient(pat); mwlItems.add(mwlItem); LOG.info("Move {} from {} to {}", mwlItem, prior, pat); } } private void moveModalityPerformedProcedureSteps(Patient pat, Patient prior) { Collection<MPPS> mppss = (pat.getModalityPerformedProcedureSteps() != null ? pat .getModalityPerformedProcedureSteps() : new ArrayList<MPPS>()); for (Iterator<MPPS> iter = (prior.getModalityPerformedProcedureSteps() != null ? prior .getModalityPerformedProcedureSteps().iterator() : new ArrayList<MPPS>().iterator()); iter.hasNext();) { MPPS mpps = iter.next(); iter.remove(); mpps.setPatient(pat); mppss.add(mpps); LOG.info("Move {} from {} to {}", mpps, prior, pat); } } private boolean movePatientIDs(Patient pat, Patient prior, Collection<IDWithIssuer> priorPIDs) { int moved = 0; Collection<PatientID> patientIDs = pat.getPatientIDs(); for (Iterator<PatientID> iter = prior.getPatientIDs().iterator(); iter .hasNext();) { PatientID patientID = iter.next(); if (!contains(priorPIDs, patientID.toIDWithIssuer())) { iter.remove(); patientID.setPatient(pat); patientIDs.add(patientID); LOG.info("Move {} from {} to {}", patientID, prior, pat); moved++; } } return moved > 0; } private boolean contains(Collection<IDWithIssuer> pids, IDWithIssuer pid) { for (IDWithIssuer pid1 : pids) { if (pid1.matches(pid)) return true; } return false; } @Override public void updatePatientID(Attributes srcPatientAttrs, Attributes otherPatientAttrs, StoreParam storeParam) throws NonUniquePatientException { Collection<IDWithIssuer> pids = IDWithIssuer.pidsOf(srcPatientAttrs); Collection<IDWithIssuer> otherPids = IDWithIssuer .pidsOf(otherPatientAttrs); List<Patient> srcPatient; List<Patient> otherPatient; srcPatient = findPatientByIDs(pids); if (srcPatient.isEmpty()) throw new IllegalArgumentException( "No patient found for source attributes - expected 1 match"); else if (srcPatient.size() > 1) throw new IllegalArgumentException( "More than one patient found for source attributes - expected 1 match"); Patient patient = srcPatient.get(0); otherPatient = findPatientByIDs(otherPids); if (!otherPatient.isEmpty()) throw new IllegalArgumentException( "One or more patients found for target update attributes - expected 0 match"); // check if original ids have links ArrayList<IDWithIssuer> linkedPatientsIDs = new ArrayList<IDWithIssuer>(); for (PatientID linkedID : patient.getLinkedPatientIDs()) { linkedPatientsIDs.add(linkedID.toIDWithIssuer()); } Collection<Patient> linkedPatients = findPatientByIDs(linkedPatientsIDs); Iterator<Patient> linkedPatientsIterator = linkedPatients.iterator(); try { while (linkedPatientsIterator.hasNext()) { Patient other = linkedPatientsIterator.next(); unlinkPatientIDs(patient, other, storeParam.isDeIdentifyLogs()); unlinkPatientIDs(other, patient, storeParam.isDeIdentifyLogs()); } patient.getPatientIDs().clear(); Collection<PatientID> newPatientIDs = createPatientIDs(otherPids, patient, storeParam.isDeIdentifyLogs()); for (PatientID id : newPatientIDs) { patient.getPatientIDs().add(id); } } catch (Exception e) { em.clear(); LOG.info("failed to update ID {} with {} ", pids, otherPids); } // set blob data Attributes patientAttrs = patient.getAttributes(); AttributeFilter filter = storeParam.getAttributeFilter(Entity.Patient); if (Utils.updateAttributes(patientAttrs, otherPatientAttrs, null, filter, MetadataUpdateStrategy.COERCE_MERGE)) { patient.setAttributes(patientAttrs, filter, storeParam.getFuzzyStr(), storeParam.getNullValueForQueryFields()); em.flush(); LOG.info("Update ID {} with {} ", pids, otherPids); } } }