/*
* *** 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) 2015
* 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.mpps.emulate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
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.ElementDictionary;
import org.dcm4che3.data.Sequence;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.UID;
import org.dcm4che3.data.VR;
import org.dcm4che3.net.ApplicationEntity;
import org.dcm4che3.net.Device;
import org.dcm4che3.net.Dimse;
import org.dcm4che3.net.service.DicomServiceException;
import org.dcm4che3.util.DateUtils;
import org.dcm4che3.util.UIDUtils;
import org.dcm4chee.archive.conf.ArchiveAEExtension;
import org.dcm4chee.archive.conf.ArchiveDeviceExtension;
import org.dcm4chee.archive.conf.Entity;
import org.dcm4chee.archive.conf.MPPSCreationRule;
import org.dcm4chee.archive.conf.StoreParam;
import org.dcm4chee.archive.entity.Instance;
import org.dcm4chee.archive.entity.MPPS;
import org.dcm4chee.archive.entity.Patient;
import org.dcm4chee.archive.entity.Series;
import org.dcm4chee.archive.entity.Study;
import org.dcm4chee.archive.mpps.MPPSContext;
import org.dcm4chee.archive.mpps.MPPSService;
import org.dcm4chee.archive.store.session.StudyUpdatedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Gunter Zeilinger <gunterze@gmail.com>
* @author Umberto Cappellini
* @author Roman K
*/
@Stateless
public class MPPSEmulatorEJB {
public static final class EmulationResult {
public final ApplicationEntity ae;
public final MPPS mpps;
public EmulationResult(ApplicationEntity ae, MPPS mpps) {
this.ae = ae;
this.mpps = mpps;
}
}
@PersistenceContext(name = "dcm4chee-arc", unitName = "dcm4chee-arc")
private EntityManager em;
@Inject
Device device;
@Inject
MPPSService mppsService;
private static final Logger LOG = LoggerFactory.getLogger(MPPSEmulatorEJB.class);
private static final int[] PATIENT_Selection = {
Tag.SpecificCharacterSet,
Tag.PatientName,
Tag.PatientID,
Tag.IssuerOfPatientID,
Tag.PatientBirthDate,
Tag.PatientSex };
private static final int[] SERIES_Selection = {
Tag.SeriesInstanceUID,
Tag.SeriesDescription,
Tag.PerformingPhysicianName,
Tag.ProtocolName,
Tag.OperatorsName,
Tag.RetrieveAETitle
};
private static final int[] STUDY_Selection = {
Tag.ProcedureCodeSequence,
Tag.StudyID };
private static final int[] MPPS_SET_Selection = {
Tag.SpecificCharacterSet,
Tag.SOPInstanceUID,
Tag.PerformedProcedureStepEndDate,
Tag.PerformedProcedureStepEndTime,
Tag.PerformedSeriesSequence,
Tag.PerformedProcedureStepDescription,
Tag.PerformedProcedureTypeDescription,
Tag.ProcedureCodeSequence,
Tag.PerformedProtocolCodeSequence
};
static {
Arrays.sort(PATIENT_Selection);
Arrays.sort(SERIES_Selection);
Arrays.sort(STUDY_Selection);
Arrays.sort(MPPS_SET_Selection);
}
private static final ElementDictionary dict = ElementDictionary.getStandardElementDictionary();
/**
* Checks configured rule, finds the series, emulates MPPS
* @param studyUpdatedEvent
* @return
* @throws DicomServiceException
*/
public MPPS emulateMPPS(StudyUpdatedEvent studyUpdatedEvent) throws DicomServiceException {
// find all series that are to be affected by this emulated MPPS
List<Series> seriesList = findAffectedSeries(studyUpdatedEvent);
if (seriesList == null) return null;
// there could be multiple local AETs used - just get the first one for providing configuration for MPPS service
String localAET = studyUpdatedEvent.getLocalAETs().iterator().next();
// checks if emulated MPPS should be created, according to the configured rule
MPPSCreationRule creationRule = device
.getDeviceExtensionNotNull(ArchiveDeviceExtension.class)
.getMppsEmulationRule(studyUpdatedEvent.getSourceAET())
.getCreationRule();
if (!checkCreationRule(creationRule, seriesList)) return null;
LOG.info("Emulate MPPS for Study[iuid={}] received from {}", studyUpdatedEvent.getStudyInstanceUID(), studyUpdatedEvent.getSourceAET());
String mppsIUID = UIDUtils.createUID();
ApplicationEntity ae = device.getApplicationEntityNotNull(localAET);
ArchiveAEExtension arcAE = ae.getAEExtensionNotNull(ArchiveAEExtension.class);
updateMPPSReferences(mppsIUID, seriesList, arcAE.getStoreParam());
// prepare attrs
Attributes mppsCreateAttributes = makeMPPSCreateAttributes(seriesList, mppsIUID);
Attributes completedAttributes = makeMPPSUpdateCompletedAttributes(mppsCreateAttributes);
// create MPPS
if (mppsCreateAttributes == null)
return null;
MPPSContext mppsContext = new MPPSContext(studyUpdatedEvent.getSourceAET(), localAET, mppsIUID, Dimse.N_CREATE_RQ);
mppsService.createPerformedProcedureStep(mppsCreateAttributes, mppsContext);
// update MPPS with status COMPLETED
MPPSContext mppsCompletedContext = new MPPSContext(studyUpdatedEvent.getSourceAET(), localAET, mppsIUID, Dimse.N_SET_RQ);
mppsService.updatePerformedProcedureStep(completedAttributes, mppsCompletedContext);
ArrayList<String> refMppsList = new ArrayList<>();
refMppsList.add(mppsIUID);
List<MPPS> mppsList = fetchMPPS(refMppsList);
return mppsList.get(0);
}
public List<Series> findAffectedSeries(StudyUpdatedEvent studyUpdatedEvent) {
List<Series> seriesList = em
.createNamedQuery(
Series.FIND_BY_STUDY_INSTANCE_UID_AND_SOURCE_AET,
Series.class).setParameter(1, studyUpdatedEvent.getStudyInstanceUID())
.setParameter(2, studyUpdatedEvent.getSourceAET()).getResultList();
if (seriesList.isEmpty())
return null;
// filter series list - leave only ones affected by this StudyUpdateEvent
Iterator<Series> seriesIterator = seriesList.iterator();
while (seriesIterator.hasNext())
if (!studyUpdatedEvent.getAffectedSeriesUIDs().contains(seriesIterator.next().getSeriesInstanceUID()))
seriesIterator.remove();
return seriesList;
}
public void updateMPPSReferences(String mppsIUID, List<Series> series, StoreParam storeParam) {
for (Series ser : series) {
Attributes serAttrs = ser.getAttributes();
Attributes mppsRef = new Attributes(2);
mppsRef.setString(Tag.ReferencedSOPClassUID, VR.UI, UID.ModalityPerformedProcedureStepSOPClass);
mppsRef.setString(Tag.ReferencedSOPInstanceUID, VR.UI, mppsIUID);
serAttrs.newSequence(Tag.ReferencedPerformedProcedureStepSequence, 1).add(mppsRef);
ser.setAttributes(serAttrs,
storeParam.getAttributeFilter(Entity.Series),
storeParam.getFuzzyStr(),
storeParam.getNullValueForQueryFields());
em.merge(ser);
}
}
private Attributes makeMPPSCreateAttributes(List<Series> seriesList, String mppsSOPInstanceUID) {
Attributes mppsAttrs = new Attributes();
Series firstSeries = seriesList.get(0);
Study study = firstSeries.getStudy();
Patient patient = study.getPatient();
String modality = firstSeries.getModality() == null ? "OT" : firstSeries.getModality();
// pps information
mppsAttrs.setString(Tag.PerformedProcedureStepStatus, VR.CS, MPPS.IN_PROGRESS);
mppsAttrs.addSelected(patient.getAttributes(), PATIENT_Selection);
mppsAttrs.addSelected(study.getAttributes(), STUDY_Selection);
if (!mppsAttrs.contains(Tag.ProcedureCodeSequence))
mppsAttrs.newSequence(Tag.ProcedureCodeSequence, 0);
mppsAttrs.setString(Tag.SOPInstanceUID, VR.UI, mppsSOPInstanceUID);
mppsAttrs.setString(Tag.SOPClassUID, VR.UI, UID.ModalityPerformedProcedureStepSOPClass);
mppsAttrs.setString(Tag.PerformedStationAETitle, VR.AE, firstSeries.getSourceAET());
mppsAttrs.setString(Tag.PerformedStationName, VR.SH, firstSeries.getStationName());
mppsAttrs.setNull(Tag.PerformedLocation, VR.SH);
mppsAttrs.setString(Tag.Modality, VR.CS, modality);
mppsAttrs.setString(Tag.PerformedProcedureStepID, VR.SH, makePPSID(modality, study.getStudyInstanceUID()));
mppsAttrs.setString(Tag.PerformedProcedureStepDescription, VR.LO, study.getStudyDescription());
mppsAttrs.setNull(Tag.PerformedProcedureTypeDescription, VR.LO);
mppsAttrs.newSequence(Tag.ReferencedPatientSequence, 0);
mppsAttrs.newSequence(Tag.PerformedProtocolCodeSequence, 0);
// scheduled step attributes sequence
// TODO scheduled/unscheduled
Sequence schedStepAttSq = mppsAttrs.newSequence(Tag.ScheduledStepAttributesSequence, 1);
Attributes ssasItem = new Attributes();
ssasItem.setString(Tag.StudyInstanceUID, VR.UI, study.getStudyInstanceUID());
ssasItem.setString(Tag.AccessionNumber, VR.SH, study.getAccessionNumber());
ssasItem.setNull(Tag.RequestedProcedureID, VR.SH);
ssasItem.setNull(Tag.RequestedProcedureDescription, VR.LO);
ssasItem.setNull(Tag.ScheduledProcedureStepID, VR.SH);
ssasItem.setNull(Tag.ScheduledProcedureStepDescription, VR.LO);
ssasItem.newSequence(Tag.ScheduledProtocolCodeSequence, 0);
ssasItem.newSequence(Tag.ReferencedStudySequence, 0);
schedStepAttSq.add(ssasItem);
// performed series sequence
Sequence perfSeriesSq = mppsAttrs.newSequence(Tag.PerformedSeriesSequence, seriesList.size());
Date start_date = null, end_date = null;
for (Series series : seriesList) {
if (series.getInstances().isEmpty())
continue;
Attributes pssqItem = new Attributes();
for (int tag : SERIES_Selection) // ensure all type 2 tags are set
pssqItem.setNull(tag, dict.vrOf(tag));
pssqItem.addSelected(series.getAttributes(), SERIES_Selection);
if (!pssqItem.containsValue(Tag.ProtocolName))
pssqItem.setString(Tag.ProtocolName, VR.LO, "UNKNOWN");
Sequence refImgSq = pssqItem.newSequence(Tag.ReferencedImageSequence, series.getInstances().size());
for (Instance inst : series.getInstances()) {
start_date = chooseDate(start_date, inst.getCreatedTime(), false);
end_date = chooseDate(end_date, inst.getCreatedTime(), true);
Attributes refImg = new Attributes();
refImg.setString(Tag.ReferencedSOPClassUID, VR.UI, inst.getSopClassUID());
refImg.setString(Tag.ReferencedSOPInstanceUID, VR.UI, inst.getSopInstanceUID());
refImgSq.add(refImg);
}
// TODO shouldn't we add non-images to ReferencedNonImageCompositeSOPInstanceSequence instead of ReferencedImageSequence?
pssqItem.newSequence(Tag.ReferencedNonImageCompositeSOPInstanceSequence, 0);
perfSeriesSq.add(pssqItem);
}
if (start_date == null) {
LOG.info("No instances available! Skip MPPS emulation.");
return null;
}
// pps datetime
mppsAttrs.setString(Tag.PerformedProcedureStepStartDate, VR.DA, DateUtils.formatDA(null, start_date));
mppsAttrs.setString(Tag.PerformedProcedureStepStartTime, VR.TM, DateUtils.formatTM(null, start_date));
mppsAttrs.setString(Tag.PerformedProcedureStepEndDate, VR.DA, DateUtils.formatDA(null, end_date));
mppsAttrs.setString(Tag.PerformedProcedureStepEndTime, VR.TM, DateUtils.formatTM(null, end_date));
return mppsAttrs;
}
private Attributes makeMPPSUpdateCompletedAttributes(Attributes mppsCreateAttributes) {
Attributes attributes = new Attributes();
attributes.addSelected(mppsCreateAttributes, MPPS_SET_Selection);
attributes.setString(Tag.PerformedProcedureStepStatus, VR.CS, MPPS.COMPLETED);
return attributes;
}
private boolean checkCreationRule(MPPSCreationRule rule,
List<Series> seriesList) {
// if rule is NEVER, then just return
if (rule.equals(MPPSCreationRule.NEVER)) {
LOG.debug("MPPS creation rule is NEVER => NOT emulating MPPS.");
return false;
}
// check if referenced mpps exists
List<String> refMppsList = new ArrayList<String>();
for (Series series : seriesList) {
Sequence refPpsSeq = series.getAttributes().getSequence(Tag.ReferencedPerformedProcedureStepSequence);
if (refPpsSeq != null
&& refPpsSeq.size() > 0
&& refPpsSeq.get(0).getString(Tag.ReferencedSOPInstanceUID) != null)
refMppsList.add(refPpsSeq.get(0).getString(Tag.ReferencedSOPInstanceUID));
}
if (refMppsList.size() == 0) {
LOG.debug("No reference to existing MPPS found => emulating MPPS...");
return true;
}
// cases when there is an existing reference (refMppsList.size()>0)
List<MPPS> mppsList = null;
switch (rule) {
case ALWAYS:
LOG.debug("MPPS references found and rule is ALWAYS => emulating MPPS...");
deleteMPPS(refMppsList);
return true;
case NEVER:
// will never hit this line, see above
return false;
case NO_MPPS_CREATE:
mppsList = fetchMPPS(refMppsList);
if (mppsList == null || mppsList.size() == 0) {
LOG.debug("MPPS references found, no mpps stored and rule NO_MPPS_CREATE => emulating MPPS...");
return true;
} else {
LOG.debug("MPPS references found, mpps stored and rule NO_MPPS_CREATE => NOT emulating MPPS.");
return false;
}
case NO_MPPS_FINAL:
mppsList = fetchMPPS(refMppsList);
for (MPPS mpps : mppsList) {
if (mpps.getStatus() == MPPS.Status.IN_PROGRESS) {
LOG.debug("MPPS reference found, mpps stored, at least one MPPS not finalized and rule NO_MPPS_FINAL => emulating MPPS...");
deleteMPPS(refMppsList);
return true;
}
}
LOG.debug("MPPS reference found, mpps stored, all MPPS finalized and rule NO_MPPS_FINAL => NOT emulating MPPS.");
return false;
default:
return true; // default = emulate MPPS
}
}
private List<MPPS> fetchMPPS(List<String> refMppsList) {
return (refMppsList.size() > 0) ? em
.createNamedQuery(MPPS.FIND_BY_SOP_INSTANCE_UIDs, MPPS.class)
.setParameter("idList", refMppsList).getResultList() : null;
}
private void deleteMPPS(List<String> refMppsList) {
if (refMppsList.size() > 0)
em.createNamedQuery(MPPS.DELETE_BY_SOP_INSTANCE_UIDs)
.setParameter("idList", refMppsList).executeUpdate();
}
private String makePPSID(String modality, String studyInstanceUID) {
return modality.substring(0, 2)
+ studyInstanceUID.substring(Math.max(0,
studyInstanceUID.length() - 14));
}
private Date chooseDate(Date date1, Date date2, boolean returnMostRecent) {
if (date1 == null)
return date2;
if (date2 == null)
return date1;
if (date1.compareTo(date2) > 0)
return returnMostRecent ? date1 : date2;
else
return returnMostRecent ? date2 : date1;
}
}