/* ***** 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.mima.impl; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.enterprise.context.ApplicationScoped; 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.Issuer; import org.dcm4che3.data.Sequence; import org.dcm4che3.data.Tag; import org.dcm4che3.data.VR; import org.dcm4chee.archive.conf.ArchiveAEExtension; import org.dcm4chee.archive.conf.ArchiveDeviceExtension; import org.dcm4chee.archive.entity.QPatient; import org.dcm4chee.archive.entity.Utils; import org.dcm4chee.archive.query.util.QueryBuilder; import org.hibernate.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mysema.query.BooleanBuilder; import com.mysema.query.Tuple; import com.mysema.query.jpa.hibernate.HibernateQuery; /** * Attributes coercion according to MIMA specs. * * @author Gunter Zeilinger <gunterze@gmail.com> * @author Umberto Cappellini <umberto.cappellini@agfa.com> * */ @ApplicationScoped class MIMAAttributeCoercion { private static Logger LOG = LoggerFactory .getLogger(MIMAAttributeCoercion.class); @PersistenceContext(name = "dcm4chee-arc", unitName = "dcm4chee-arc") private EntityManager em; @Inject private PIXConsumer pixConsumer; public void coerce(ArchiveAEExtension arcAE, MIMAInfo info, Attributes attrs) { coercePatientIDsAndPatientNames(arcAE, info, attrs); coerceAccessionNumber(info, attrs); } private void coercePatientIDsAndPatientNames(ArchiveAEExtension arcAE, MIMAInfo info, Attributes attrs) { // ids contained in the response instance Set<IDWithIssuer> pids = IDWithIssuer.pidsOf(attrs); if (pids == null || pids.size() == 0) return; IDWithIssuer coercedRootID = null; Set<IDWithIssuer> coercedOtherIDs = new HashSet<IDWithIssuer>(); Issuer requestedIssuer = info.getRequestedIssuerOfPatientID(); if (requestedIssuer == null && !info.isReturnOtherPatientIDs() && !info.isReturnOtherPatientNames()) return; // group together all the patient's pids and the linked ids of each pid, // avoiding re-running a pix query when not necessary (i.e. using the // cached PIX responses). for (IDWithIssuer pid : pids) { // first, add the id itself, if not already added. // if the pid is already found in the list, all the rest is skipped // as we consider the pix query to be simmetrical and we don't need // to perform the reverse pix query if (!containsMatch(coercedOtherIDs, pid)) { coercedOtherIDs.add(pid); // Second, search for linked ids. // If a PIX query response already containing the pid is found // in // the cache, reuse the cached PIX response, otherwise make a // new // PIX query IDWithIssuer[] linkedIDs = info.getCachedPixResponse(pid); if (linkedIDs == null) { linkedIDs = pixConsumer.pixQuery(arcAE, pid); info.cachePixResponse(linkedIDs); } // third, add the linked ids, if not already added. for (IDWithIssuer linkedID : linkedIDs) if (!containsMatch(coercedOtherIDs, linkedID)) coercedOtherIDs.add(linkedID); } } // calculate the returned root id, i.e. the id among the collected // coercedOtherIDs having the requested issuer (if any) if (requestedIssuer != null) { for (IDWithIssuer pid : coercedOtherIDs) if (pid.getIssuer() != null && requestedIssuer.matches(pid.getIssuer())) { coercedRootID = pid; break; } if (coercedRootID == null) { attrs.setNull(Tag.PatientID, VR.LO); requestedIssuer.toIssuerOfPatientID(attrs); LOG.info("Nullify Patient ID for requested Issuer: {}", requestedIssuer); } else { IDWithIssuer originalRootID = IDWithIssuer.pidOf(attrs); if (!coercedRootID.matches(originalRootID)) { requestedIssuer.toIssuerOfPatientID(attrs); attrs.setString(Tag.PatientID, VR.LO, coercedRootID.getID()); LOG.info("Adjust Patient ID to {}", coercedRootID); } } } // if requested, add the whole set of other patient ids to the response if (info.isReturnOtherPatientIDs()) { addOtherPatientIDs(attrs, coercedOtherIDs); } // if requested, add the whole set of patient names to the response if (info.isReturnOtherPatientNames()) { ArchiveDeviceExtension dE = arcAE.getApplicationEntity().getDevice() .getDeviceExtension(ArchiveDeviceExtension.class); boolean deIdentifyLogs = (dE != null) ? dE.isDeIdentifyLogs() : false; addOtherPatientNames(attrs, coercedOtherIDs, info, deIdentifyLogs); } } private static void addOtherPatientIDs(Attributes attrs, Set<IDWithIssuer> pids) { Sequence seq = attrs.newSequence(Tag.OtherPatientIDsSequence, pids.size()); for (IDWithIssuer pid : pids) seq.add(pid.exportPatientIDWithIssuer(null)); LOG.info("Add Other Patient IDs: {}", Arrays.toString(pids.toArray())); } private void addOtherPatientNames(Attributes match, Set<IDWithIssuer> pids, MIMAInfo info, boolean deIdentifyLogs) { String[] patientNames = info.getPatientNamesFromCache(pids); if (patientNames == null) { patientNames = queryPatientNames(pids.toArray(new IDWithIssuer[pids .size()])); info.cachePatientNames(pids, patientNames); } match.setString(Tag.OtherPatientNames, VR.PN, patientNames); if (deIdentifyLogs) LOG.info("Add" + patientNames.length + " Other Patient Names"); else LOG.info("Add Other Patient Names: {}",Arrays.toString(patientNames)); } private String[] queryPatientNames(IDWithIssuer[] pids) { HashSet<String> c = new HashSet<String>(pids.length * 4 / 3 + 1); BooleanBuilder builder = new BooleanBuilder(); builder.and(QueryBuilder.pids(pids, false, false)); builder.and(QPatient.patient.mergedWith.isNull()); List<Tuple> tuples = new HibernateQuery(em.unwrap(Session.class)) .from(QPatient.patient).where(builder) .list(QPatient.patient.pk, QPatient.patient.attributesBlob.encodedAttributes); for (Tuple tuple : tuples) c.add(Utils.decodeAttributes(tuple.get(1, byte[].class)).getString( Tag.PatientName)); c.remove(null); return c.toArray(new String[c.size()]); } private void coerceAccessionNumber(MIMAInfo info, Attributes attrs) { Issuer requestedIssuer = info.getRequestedIssuerOfAccessionNumber(); if (requestedIssuer != null) { adjustAccessionNumber(requestedIssuer, attrs); Sequence rqAttrsSeq = attrs .getSequence(Tag.RequestAttributesSequence); if (rqAttrsSeq != null) { for (Attributes rqAttrs : rqAttrsSeq) adjustAccessionNumber(requestedIssuer, rqAttrs); } } } private void adjustAccessionNumber(Issuer requestedIssuer, Attributes attrs) { if (!attrs.containsValue(Tag.AccessionNumber)) return; Issuer issuer = Issuer.valueOf(attrs .getNestedDataset(Tag.IssuerOfAccessionNumberSequence)); if (issuer == null || !requestedIssuer.matches(issuer)) { attrs.setNull(Tag.AccessionNumber, VR.SH); LOG.info("Nullify Accession Number for requested Issuer: {}", requestedIssuer); } attrs.newSequence(Tag.IssuerOfAccessionNumberSequence, 1).add( requestedIssuer.toItem()); } private boolean containsMatch(Set<IDWithIssuer> set, IDWithIssuer id) { if (set == null || id == null) return false; boolean contains = false; for (IDWithIssuer idOfSet : set) if (idOfSet != null && idOfSet.matches(id)) { contains = true; break; } return contains; } }