/* ***** 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.noniocm.impl;
import static java.lang.String.format;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.IDWithIssuer;
import org.dcm4che3.data.Sequence;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.VR;
import org.dcm4che3.net.Device;
import org.dcm4che3.util.UIDUtils;
import org.dcm4chee.archive.code.CodeService;
import org.dcm4chee.archive.conf.Entity;
import org.dcm4chee.archive.conf.NoneIOCMChangeRequestorExtension;
import org.dcm4chee.archive.conf.StoreAction;
import org.dcm4chee.archive.dto.ActiveService;
import org.dcm4chee.archive.entity.ActiveProcessing;
import org.dcm4chee.archive.entity.Code;
import org.dcm4chee.archive.entity.Instance;
import org.dcm4chee.archive.entity.history.ActionHistory;
import org.dcm4chee.archive.entity.history.InstanceHistory;
import org.dcm4chee.archive.entity.history.SeriesHistory;
import org.dcm4chee.archive.entity.history.StudyHistory;
import org.dcm4chee.archive.noniocm.NonIOCMChangeRequestorService;
import org.dcm4chee.archive.noniocm.NonIocmChangeRequestorMDB;
import org.dcm4chee.archive.patient.PatientCircularMergedException;
import org.dcm4chee.archive.patient.PatientSelectorFactory;
import org.dcm4chee.archive.patient.PatientService;
import org.dcm4chee.archive.processing.ActiveProcessingService;
import org.dcm4chee.archive.qc.QCEvent.QCOperation;
import org.dcm4chee.archive.qc.QCOperationNotPermittedException;
import org.dcm4chee.archive.qc.StructuralChangeService;
import org.dcm4chee.archive.sc.STRUCTURAL_CHANGE;
import org.dcm4chee.archive.store.StoreContext;
import org.dcm4chee.archive.store.StoreSession;
import org.dcm4chee.archive.store.session.StudyUpdatedEvent;
import org.dcm4chee.archive.studyprotection.StudyProtectionHook;
import org.dcm4chee.hooks.Hooks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Franz Willer <franz.willer@gmail.com>
* @author Hesham Elbadawi <bsdreko@gmail.com>
*/
@Stateless
public class NonIOCMChangeRequestorServiceEJB implements NonIOCMChangeRequestorService {
//Array must be sorted!
private static final int[] BASIC_CHG_ATTRIBUTES = new int[]{Tag.SOPInstanceUID, Tag.PatientID,Tag.IssuerOfPatientID, Tag.IssuerOfPatientIDQualifiersSequence, Tag.StudyInstanceUID, Tag.SeriesInstanceUID};
private static final List<ActiveService> NON_IOCM_ACTIVE_SERVICES = Arrays.asList(ActiveService.NON_IOCM_UPDATE);
private static final Logger LOG = LoggerFactory.getLogger(NonIOCMChangeRequestorServiceEJB.class);
@Inject
private ActiveProcessingService activeProcessingService;
@Inject
private StructuralChangeService scService;
@Inject
private Hooks<StudyProtectionHook> studyProtectionHooks;
@Inject
private PatientService patientService;
@Inject
private CodeService codeService;
@Resource(mappedName = "java:/JmsXA")
private ConnectionFactory connFactory;
@Resource(mappedName = "java:/queue/noneiocm")
private Queue noniocmQueue;
@Inject
private Device device;
@PersistenceContext(name="dcm4chee-arc")
private EntityManager em;
@Override
public boolean isNonIOCMChangeRequestor(String callingAET) {
return getNonIOCMDevice(callingAET) != null;
}
@Override
public boolean isNonIOCMChangeRequest(String callingAET, String sourceAET) {
Device d = getNonIOCMDevice(callingAET);
return d == null ? false : callingAET.equals(sourceAET) ? true : d.getApplicationAETitles().contains(sourceAET);
}
@Override
public int getNonIOCMModalityGracePeriod(String callingAET) {
NoneIOCMChangeRequestorExtension ext = device.getDeviceExtension(NoneIOCMChangeRequestorExtension.class);
if (ext == null || ext.getNoneIOCMModalityDevices().isEmpty()) {
LOG.debug("No NoneIOCMModalityDevices configured!");
return Integer.MIN_VALUE;
}
for (Device d : ext.getNoneIOCMModalityDevices()) {
if (d.getApplicationAETitles().contains(callingAET))
return ext.getGracePeriod();
}
return -1;
}
private Device getNonIOCMDevice(String callingAET) {
if (callingAET != null) {
NoneIOCMChangeRequestorExtension ext = device.getDeviceExtension(NoneIOCMChangeRequestorExtension.class);
if (ext == null || ext.getNoneIOCMChangeRequestorDevices().isEmpty()) {
LOG.debug("No NoneIOCMChangeRequestorDevices configured!");
return null;
}
for (Device d : ext.getNoneIOCMChangeRequestorDevices()) {
if (d != null && d.getApplicationAETitles().contains(callingAET)) {
return d;
}
}
}
return null;
}
@Override
public NonIOCMChangeType getChangeType(Instance inst, Attributes attrs) {
Attributes patAttrs = inst.getSeries().getStudy().getPatient().getAttributes();
IDWithIssuer currentPID = IDWithIssuer.pidOf(patAttrs);
return getChangeType(currentPID, inst.getSeries().getStudy().getStudyInstanceUID(), inst.getSeries().getSeriesInstanceUID(), inst.getSopInstanceUID(), attrs);
}
private NonIOCMChangeType getChangeType(IDWithIssuer currentPID, String studyIUID, String seriesIUID, String sopIUID, Attributes attrs) {
if (!sopIUID.equals(attrs.getString(Tag.SOPInstanceUID)))
throw new IllegalArgumentException("Current and new Instance must have the same SOP Instance UID!");
boolean seriesChg = !seriesIUID.equals(attrs.getString(Tag.SeriesInstanceUID));
boolean studyChg = !studyIUID.equals(attrs.getString(Tag.StudyInstanceUID));
boolean patIDChg = !currentPID.equals(IDWithIssuer.pidOf(attrs));
if (patIDChg) {
if (studyChg || seriesChg) {
LOG.warn("Illegal NoneICOM PatID change request! Study IUID and Series IUID must not be changed!");
return NonIOCMChangeType.ILLEGAL_CHANGE;
}
return NonIOCMChangeType.PAT_ID_CHANGE;
} else if (studyChg) {
if (seriesChg) {
LOG.warn("Illegal NoneICOM Study IUID change request! Series IUID must not be changed!");
return NonIOCMChangeType.ILLEGAL_CHANGE;
}
return NonIOCMChangeType.STUDY_IUID_CHANGE;
} else if (seriesChg) {
return NonIOCMChangeType.SERIES_IUID_CHANGE;
}
return NonIOCMChangeType.INSTANCE_CHANGE;
}
public List<InstanceHistory> findInstanceHistory(String sopInstanceUID) {
return new ArrayList<InstanceHistory>();
}
@Override
public NonIOCMChangeType performChange(Instance inst, StoreContext context) {
Attributes chgAttrs = new Attributes(context.getAttributes(), BASIC_CHG_ATTRIBUTES);
Attributes origAttrs = new Attributes();
Sequence origSQ = chgAttrs.ensureSequence(Tag.ModifiedAttributesSequence, 1);
origSQ.add(origAttrs);
NonIOCMChangeType chgType = getChangeType(inst, context.getAttributes());
LOG.debug("performChange start for changeType:{}", chgType);
switch (chgType) {
case PAT_ID_CHANGE:
origAttrs.addSelected(inst.getSeries().getStudy().getPatient().getAttributes(),
context.getStoreSession().getStoreParam().getAttributeFilter(Entity.Patient).getSelection());
StoreSession session = context.getStoreSession();
try {
patientService.updateOrCreatePatientOnCStore(context.getAttributes(),
PatientSelectorFactory.createSelector(session.getStoreParam()), session.getStoreParam());
} catch (PatientCircularMergedException e) {
LOG.error("Patient for received Instance is merged circular!", e);
}
activeProcessingService.addActiveProcess(inst.getSeries().getStudy().getStudyInstanceUID(),
inst.getSeries().getSeriesInstanceUID(), inst.getSopInstanceUID(), ActiveService.NON_IOCM_UPDATE, chgAttrs);
break;
case STUDY_IUID_CHANGE:
origAttrs.setString(Tag.StudyInstanceUID, VR.UI, inst.getSeries().getStudy().getStudyInstanceUID());
activeProcessingService.addActiveProcess(inst.getSeries().getStudy().getStudyInstanceUID(),
inst.getSeries().getSeriesInstanceUID(), inst.getSopInstanceUID(), ActiveService.NON_IOCM_UPDATE, chgAttrs);
break;
case SERIES_IUID_CHANGE:
origAttrs.setString(Tag.SeriesInstanceUID, VR.UI, inst.getSeries().getSeriesInstanceUID());
activeProcessingService.addActiveProcess(inst.getSeries().getStudy().getStudyInstanceUID(),
inst.getSeries().getSeriesInstanceUID(), inst.getSopInstanceUID(), ActiveService.NON_IOCM_UPDATE, chgAttrs);
break;
case INSTANCE_CHANGE:
origAttrs.setString(Tag.SOPInstanceUID, VR.UI, inst.getSopInstanceUID());
String newUID = UIDUtils.createUID();
context.getAttributes().setString(Tag.SOPInstanceUID, VR.UI, newUID);
context.setProperty(StoreServiceNonIOCMDecorator.NONE_IOCM_HIDE_NEW_INSTANCE, newUID);
chgAttrs.setString(Tag.SOPInstanceUID, VR.UI, newUID);
activeProcessingService.addActiveProcess(inst.getSeries().getStudy().getStudyInstanceUID(),
inst.getSeries().getSeriesInstanceUID(), newUID, ActiveService.NON_IOCM_UPDATE, chgAttrs);
break;
default:
}
return chgType;
}
@Override
public InstanceHistory getLastQCInstanceHistory(String sopIUID) {
Query query = em.createNamedQuery(InstanceHistory.FIND_BY_OLD_UID);
query.setParameter(1, sopIUID);
@SuppressWarnings("unchecked")
List<InstanceHistory> tmp = (List<InstanceHistory>) query.getResultList();
return tmp.size() == 0 ? null : tmp.get(0);
}
@Override
public void hideOrUnhideInstance(Instance instance, org.dcm4che3.data.Code rejNoteCode) {
Code code = rejNoteCode == null ? null : codeService.findOrCreate(new Code(NonIOCMChangeRequestorService.REJ_CODE_QUALITY_REASON));
instance.setRejectionNoteCode(code);
em.merge(instance);
}
@Override
public void handleModalityChange(Instance inst, StoreContext context, int gracePeriodInSeconds) {
if(withinGracePeriodAndNonIOCMSource(inst, gracePeriodInSeconds)) {
context.setOldNONEIOCMChangeUID(inst.getSopInstanceUID());
Attributes attrs = context.getAttributes();
attrs.setString(null, Tag.SOPInstanceUID, VR.UI,
UIDUtils.createUID());
inst.setAttributes(attrs, context.getStoreSession().getStoreParam().getAttributeFilter(Entity.Instance),
context.getStoreSession().getStoreParam().getFuzzyStr(), context.getStoreSession().getStoreParam().getNullValueForQueryFields());
em.merge(inst);
context.setInstance(inst);
context.setOldNONEIOCMChangeUID(inst.getSopInstanceUID());
}
else {
context.setStoreAction(StoreAction.IGNORE);
}
}
@Override
public void onStoreInstance(StoreContext context) {
//check here if the stored instance was received by NoneIOCM
//Source modality within grace period
if(context.getOldNONEIOCMChangeUID() != null) {
//create Split QC history for none IOCM
ActionHistory action = new ActionHistory();
action.setAction(QCOperation.SPLIT.toString());
action.setCreatedTime(new Date(System.currentTimeMillis()));
em.persist(action);
StudyHistory studyHistory = new StudyHistory();
studyHistory.setAction(action);
studyHistory.setUpdatedAttributesBlob(null); //no change (expect same attrs in study)
studyHistory.setOldStudyUID(context.getAttributes().getString(Tag.StudyInstanceUID));
studyHistory.setNextStudyUID(context.getAttributes().getString(Tag.StudyInstanceUID));
em.persist(studyHistory);
SeriesHistory seriesHistory = new SeriesHistory();
seriesHistory.setStudy(studyHistory);
seriesHistory.setUpdatedAttributesBlob(null); //no change (expect same attrs in series)
seriesHistory.setOldSeriesUID(context.getAttributes().getString(Tag.SeriesInstanceUID));
em.persist(seriesHistory);
InstanceHistory instanceHistory = new InstanceHistory(
context.getAttributes().getString(Tag.StudyInstanceUID),
context.getAttributes().getString(Tag.SeriesInstanceUID),
context.getOldNONEIOCMChangeUID(),
context.getInstance().getSopInstanceUID(),
context.getInstance().getSopInstanceUID(), false);
instanceHistory.setSeries(seriesHistory);
em.persist(instanceHistory);
}
}
private boolean withinGracePeriodAndNonIOCMSource(Instance inst, int gracePeriodInSeconds) {
Query query = em.createNamedQuery(InstanceHistory
.FIND_BY_CURRENT_UID_FOR_ACTION, InstanceHistory.class);
query.setParameter(1, inst.getSopInstanceUID());
query.setParameter(2, QCOperation.DELETE.toString());
InstanceHistory foundInstanceHistory = (InstanceHistory) query.getSingleResult();
if(foundInstanceHistory == null)
return false;
if(foundInstanceHistory.getSeries().getNoneIOCMSourceAET() == null)
return false;
long createdTime = foundInstanceHistory.getSeries()
.getStudy().getAction().getCreatedTime().getTime();
long now = System.currentTimeMillis();
return (now - createdTime) < gracePeriodInSeconds;
}
@Override
public void onStudyUpdated(@Observes StudyUpdatedEvent studyUpdatedEvent) {
LOG.debug("onStudyUpdated:{}", studyUpdatedEvent);
if (isNonIOCMChangeRequestor(studyUpdatedEvent.getSourceAET())) {
String updatedStudyUID = studyUpdatedEvent.getStudyInstanceUID();
LOG.debug("Received Study-Updated event for study {} updated by a non-IOCM source", updatedStudyUID);
if (activeProcessingService.isStudyUnderProcessingByServices(updatedStudyUID, NON_IOCM_ACTIVE_SERVICES)) {
LOG.debug("Schedule NoneIOCM change request for study {}", updatedStudyUID);
try {
scheduleNonIocmChangeRequest(updatedStudyUID, 0);
} catch (JMSException e) {
LOG.error(format("Schedule of Non-IOCM-Change-Request for study %s failed!", updatedStudyUID), e);
}
} else {
LOG.debug("No active Non-IOCM service found for study {}", updatedStudyUID);
}
}
}
private void scheduleNonIocmChangeRequest(String studyIUID, int delay) throws JMSException {
Connection conn = connFactory.createConnection();
try {
Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(noniocmQueue);
Message msg = session.createMessage();
if (delay > 0) {
msg.setLongProperty("_HQ_SCHED_DELIVERY", System.currentTimeMillis() + delay);
}
msg.setStringProperty(NonIocmChangeRequestorMDB.UPDATED_STUDY_UID_MSG_PROPERTY, studyIUID);
producer.send(msg);
} finally {
conn.close();
}
}
@Override
public void processNonIOCMRequest(String processStudyIUID, List<ActiveProcessing> nonIocmProcessings) {
// check if study protected against structural changes
if (!areStructuralChangesPermittedForStudy(processStudyIUID)) {
LOG.info("Non-IOCM active-processing message for study {} will be ignored as study is protected", processStudyIUID);
return;
}
Map<String, String> sopUIDchanged = new HashMap<String, String>();
Map<String, List<String>> studyUIDchanged = new HashMap<String, List<String>>();
Map<String, List<String>> seriesUIDchanged = new HashMap<String, List<String>>();
Map<String, List<String>> patIDchanged = new HashMap<String, List<String>>();
Map<String, Attributes> patAttrChanged = new HashMap<String, Attributes>();
for (ActiveProcessing processing : nonIocmProcessings) {
Attributes attrs = processing.getAttributes();
Attributes item = attrs.getNestedDataset(Tag.ModifiedAttributesSequence);
if (item.contains(Tag.StudyInstanceUID)) {
addChgdIUID(studyUIDchanged, attrs.getString(Tag.StudyInstanceUID),
attrs.getString(Tag.SOPInstanceUID));
} else if (item.contains(Tag.SeriesInstanceUID)) {
addChgdIUID(seriesUIDchanged, attrs.getString(Tag.SeriesInstanceUID),
attrs.getString(Tag.SOPInstanceUID));
} else if (item.contains(Tag.SOPInstanceUID)) {
sopUIDchanged.put(attrs.getString(Tag.SOPInstanceUID),
item.getString(Tag.SOPInstanceUID));
} else if (item.contains(Tag.PatientID)) {
String pid = attrs.getString(Tag.PatientID) + "^^^"
+ attrs.getString(Tag.IssuerOfPatientID);
addChgdIUID(patIDchanged, pid, attrs.getString(Tag.SOPInstanceUID));
patAttrChanged.put(pid, attrs);
} else {
LOG.warn("Cannot determine Non-IOCM change! Ignore this active-processing ({})",
processing);
}
}
if (sopUIDchanged.size() > 0) {
try {
scService.replaced(STRUCTURAL_CHANGE.NON_IOCM, sopUIDchanged, REJ_CODE_QUALITY_REASON);
} catch (QCOperationNotPermittedException e1) {
LOG.warn("QC Replace-Operation not permitted", e1);
}
}
if (seriesUIDchanged.size() > 0) {
Attributes seriesAttrs = new Attributes();
for (Map.Entry<String, List<String>> e : seriesUIDchanged.entrySet()) {
seriesAttrs.setString(Tag.SeriesInstanceUID, VR.UI, e.getKey());
try {
scService.split(STRUCTURAL_CHANGE.NON_IOCM, e.getValue(), null, processStudyIUID, null, seriesAttrs, REJ_CODE_QUALITY_REASON);
} catch (QCOperationNotPermittedException e2) {
LOG.warn("QC Split-Operation not permitted", e2);
}
}
}
if (studyUIDchanged.size() > 0) {
for (Map.Entry<String, List<String>> e : studyUIDchanged.entrySet()) {
try {
scService.split(STRUCTURAL_CHANGE.NON_IOCM, e.getValue(), null, e.getKey(), null, null, REJ_CODE_QUALITY_REASON);
} catch (QCOperationNotPermittedException e3) {
LOG.warn("QC Split-Operation not permitted", e3);
}
}
}
if (patIDchanged.size() > 0) {
for (Map.Entry<String, List<String>> e : patIDchanged.entrySet()) {
IDWithIssuer pid = IDWithIssuer.pidOf(patAttrChanged.get(e.getKey()));
String studyUID = UIDUtils.createUID();
try {
scService.split(STRUCTURAL_CHANGE.NON_IOCM, e.getValue(), pid, studyUID, null, null, REJ_CODE_QUALITY_REASON);
} catch (QCOperationNotPermittedException e4) {
LOG.warn("QC Split-Operation not permitted", e4);
}
}
}
}
private static void addChgdIUID(Map<String, List<String>> map, String key, String iuid) {
List<String> chgdIUIDs = map.get(key);
if (chgdIUIDs == null) {
chgdIUIDs = new ArrayList<String>();
map.put(key, chgdIUIDs);
}
chgdIUIDs.add(iuid);
}
private boolean areStructuralChangesPermittedForStudy(String studyIUID) {
for(StudyProtectionHook studyProtectionHook : studyProtectionHooks) {
if(studyProtectionHook.isProtected(studyIUID)) {
return false;
}
}
return true;
}
}