/* ***** 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) 2013 * 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.qc.impl; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import javax.annotation.PostConstruct; import javax.ejb.EJB; import javax.ejb.EJBException; import javax.ejb.Stateless; import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.EntityNotFoundException; import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; import javax.persistence.Query; import javax.persistence.TypedQuery; import org.dcm4che3.data.Attributes; import org.dcm4che3.data.IDWithIssuer; import org.dcm4che3.data.Sequence; import org.dcm4che3.data.Tag; import org.dcm4che3.data.UID; import org.dcm4che3.data.VR; import org.dcm4che3.media.RecordFactory; import org.dcm4che3.media.RecordType; import org.dcm4che3.net.Connection; import org.dcm4che3.net.Device; import org.dcm4che3.net.service.DicomServiceException; import org.dcm4che3.util.TagUtils; import org.dcm4che3.util.UIDUtils; import org.dcm4chee.archive.code.CodeService; import org.dcm4chee.archive.conf.ArchiveAEExtension; import org.dcm4chee.archive.conf.ArchiveDeviceExtension; import org.dcm4chee.archive.conf.Entity; import org.dcm4chee.archive.conf.NoneIOCMChangeRequestorExtension; import org.dcm4chee.archive.dto.GenericParticipant; import org.dcm4chee.archive.entity.AttributesBlob; import org.dcm4chee.archive.entity.Code; import org.dcm4chee.archive.entity.ContentItem; import org.dcm4chee.archive.entity.Instance; import org.dcm4chee.archive.entity.Issuer; import org.dcm4chee.archive.entity.Location; import org.dcm4chee.archive.entity.Patient; import org.dcm4chee.archive.entity.PatientID; import org.dcm4chee.archive.entity.SeriesQueryAttributes; import org.dcm4chee.archive.entity.StudyQueryAttributes; import org.dcm4chee.archive.entity.history.*; import org.dcm4chee.archive.entity.history.ActionHistory; import org.dcm4chee.archive.entity.history.ActionHistory.HierarchyLevel; import org.dcm4chee.archive.entity.history.UpdateHistory.UpdateScope; import org.dcm4chee.archive.entity.RequestAttributes; import org.dcm4chee.archive.entity.Series; import org.dcm4chee.archive.entity.Study; import org.dcm4chee.archive.entity.VerifyingObserver; import org.dcm4chee.archive.iocm.RejectionDeleteService; import org.dcm4chee.archive.iocm.RejectionService; import org.dcm4chee.archive.iocm.client.ChangeRequestContext; import org.dcm4chee.archive.iocm.client.ChangeRequesterService; import org.dcm4chee.archive.issuer.IssuerService; import org.dcm4chee.archive.patient.PatientService; import org.dcm4chee.archive.qc.PatientCommands; import org.dcm4chee.archive.qc.QCOperationContext; import org.dcm4chee.archive.qc.QCOperationNotPermittedException; import org.dcm4chee.archive.qc.QC_OPERATION; import org.dcm4chee.archive.qc.StructuralChangeService; import org.dcm4chee.archive.sc.StructuralChangeContext.InstanceIdentifier; import org.dcm4chee.archive.sc.impl.BasicStructuralChangeContext.InstanceIdentifierImpl; import org.dcm4chee.archive.sc.impl.StructuralChangeTransactionAggregator; import org.dcm4chee.archive.store.StoreContext; import org.dcm4chee.archive.store.StoreService; import org.dcm4chee.archive.store.StoreSession; import org.dcm4chee.archive.studyprotection.StudyProtectionHook; import org.dcm4chee.archive.task.executor.impl.PlatformTaskExecutor; import org.dcm4chee.hooks.Hooks; import org.dcm4chee.util.TransactionSynchronization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Implements structural change operations. * * @author Hesham Elbadawi <bsdreko@gmail.com> * @author Alexander Hoermandinger <alexander.hoermandinger@agfa.com> */ @Stateless public class StructuralChangeServiceImpl implements StructuralChangeService { private static final Logger LOG = LoggerFactory.getLogger(StructuralChangeServiceImpl.class); private static final int[] PATIENT_AND_STUDY_ATTRS = { Tag.SpecificCharacterSet, Tag.StudyDate, Tag.StudyTime, Tag.AccessionNumber, Tag.IssuerOfAccessionNumberSequence, Tag.ReferringPhysicianName, Tag.PatientName, Tag.PatientID, Tag.IssuerOfPatientID, Tag.PatientBirthDate, Tag.PatientSex, Tag.StudyInstanceUID, Tag.StudyID }; private static final RecordFactory recordFactory = new RecordFactory(); /** * Enum ReferenceState. Signifies if an object's references were fully moved * , partially moved or none of the references were moved due to a QC * operation used by the methods for handling invisible objects (KO/PR/SR) * */ private enum ReferenceState { ALLMOVED, SOMEMOVED, NONEMOVED } @Inject private PatientService patientService; @Inject private CodeService codeService; @Inject private IssuerService issuerService; @Inject private RejectionService rejectionService; @Inject private Device device; @Inject private RejectionDeleteService rejectionServiceDeleter; @Inject protected StoreService storeService; @Inject private Hooks<StudyProtectionHook> studyProtectionHooks; @PersistenceContext(name="dcm4chee-arc", unitName ="dcm4chee-arc") private EntityManager em; private String qcSource ="Quality Control"; private ArchiveDeviceExtension archiveDeviceExtension; @Inject private StructuralChangeTransactionAggregator structuralChangeAggregator; @EJB private ChangeRequesterService changeRequester; @EJB private PlatformTaskExecutor platformExecutor; @Inject private TransactionSynchronization transactionSynchronization; /** * Triggered by the container to set the value for the archive extension. */ @PostConstruct public void init() { archiveDeviceExtension = device.getDeviceExtension(ArchiveDeviceExtension.class); } @Override public QCOperationContext mergeStudies(Enum<?> structuralChangeType, String[] sourceStudyUIDs, String targetStudyUID, Attributes targetStudyAttrs, Attributes targetSeriesAttrs, org.dcm4che3.data.Code qcRejectionCode) throws QCOperationNotPermittedException { List<Study> sourceStudies = new ArrayList<>(sourceStudyUIDs.length); for(String sourceStudyUID : sourceStudyUIDs) { Study sourceStudy = findStudy(sourceStudyUID); if(sourceStudy != null) { checkIfQCPermittedForStudy(sourceStudy); } sourceStudies.add(sourceStudy); } Study targetStudy = findStudy(targetStudyUID); if(targetStudy != null) { checkIfQCPermittedForStudy(targetStudy); } QCContextImpl.Builder mergeCtxBuilder = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.MERGE); try { for (Study sourceStudy : sourceStudies) { QCOperationContext intMergeCtx = mergeInt(sourceStudy, targetStudy, targetStudyAttrs, targetSeriesAttrs, qcRejectionCode); mergeCtxBuilder.addSourceInstances(intMergeCtx.getSourceInstances()); mergeCtxBuilder.addTargetInstances(intMergeCtx.getTargetInstances()); mergeCtxBuilder.addRejectionNotes(intMergeCtx.getRejectionNotes()); } } catch (Exception e) { LOG.error("{}: QC info[Merge] - Failure, reason {}", e); throw new EJBException(); } QCOperationContext mergeCtx = mergeCtxBuilder.build(); structuralChangeAggregator.aggregate(mergeCtx); scheduleChangeRequestAfterTxCommit(mergeCtx); return mergeCtx; } @Override public QCOperationContext merge(Enum<?> structuralChangeType, String sourceStudyUID, String targetStudyUID, Attributes targetStudyAttrs, Attributes targetSeriesAttrs, org.dcm4che3.data.Code qcRejectionCode) throws QCOperationNotPermittedException { Study source = findStudy(sourceStudyUID); Study target = findStudy(targetStudyUID); if(source == null || target == null) { LOG.error("{} : QC info[Merge] - Failure, Source Study {} or Target Study {}" + " not Found",qcSource,sourceStudyUID, targetStudyUID); throw new EJBException(); } checkIfQCPermittedForStudy(source); checkIfQCPermittedForStudy(target); QCOperationContext intMergeCtx = mergeInt(source, target, targetStudyAttrs, targetSeriesAttrs, qcRejectionCode); QCOperationContext mergeCtx = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.MERGE) .addSourceInstances(intMergeCtx.getSourceInstances()) .addTargetInstances(intMergeCtx.getTargetInstances()) .addRejectionNotes(intMergeCtx.getRejectionNotes()) .build(); structuralChangeAggregator.aggregate(mergeCtx); scheduleChangeRequestAfterTxCommit(mergeCtx); return mergeCtx; } private QCOperationContext mergeInt(Study source, Study target, Attributes targetStudyAttrs, Attributes targetSeriesAttrs, org.dcm4che3.data.Code qcRejectionCode) throws QCOperationNotPermittedException { String sourceStudyUID = source.getStudyInstanceUID(); String targetStudyUID = target.getStudyInstanceUID(); ActionHistory mergeAction = generateQCAction(QC_OPERATION.MERGE, ActionHistory.HierarchyLevel.STUDY); List<InstanceIdentifier> sourceUIDs = new ArrayList<>(); List<InstanceIdentifier> targetUIDs = new ArrayList<>(); List<Instance> rejectedInstances = new ArrayList<>(); List<InstanceHistory> instancesHistory = new ArrayList<>(); StudyHistory studyHistory = createQCStudyHistory(sourceStudyUID, targetStudyUID, target.getAttributes(), mergeAction); if(targetStudyAttrs != null && targetStudyAttrs.size() > 0) { updateStudy(archiveDeviceExtension, target, targetStudyAttrs); } for(Series series: source.getSeries()) { String seriesUID = series.getSeriesInstanceUID(); rejectedInstances.addAll(series.getInstances()); Series newSeries = createSeries(series, target, targetSeriesAttrs); String newSeriesUID = newSeries.getSeriesInstanceUID(); SeriesHistory seriesHistory = createQCSeriesHistory( seriesUID, series.getAttributes(), studyHistory, null); for(Instance inst: series.getInstances()) { String instanceUID = inst.getSopInstanceUID(); Instance newInstance = move(inst, newSeries, qcRejectionCode); String newInstanceUID = newInstance.getSopInstanceUID(); InstanceHistory instanceHistory = new InstanceHistory(targetStudyUID, newSeriesUID, instanceUID, newInstanceUID, newInstanceUID, false); instanceHistory.setSeries(seriesHistory); instancesHistory.add(instanceHistory); targetUIDs.add(new InstanceIdentifierImpl(targetStudyUID, newSeriesUID, newInstanceUID)); sourceUIDs.add(new InstanceIdentifierImpl(sourceStudyUID, seriesUID, instanceUID)); //update identical document sequence if (series.getModality().equals("KO") || series.getModality().equals("SR")) { for (Instance ident : findIdenticalDocumentReferences(newInstance)) { removeIdenticalDocumentSequence(ident, inst); removeIdenticalDocumentSequence(inst, ident); addIdenticalDocumentSequence(ident, newInstance); addIdenticalDocumentSequence(newInstance, ident); if (!identicalDocumentSequenceHistoryExists(ident, mergeAction) && !ident.getSeries().getStudy().getStudyInstanceUID() .equals(sourceStudyUID)) addIdenticalDocumentSequenceHistory(ident, mergeAction); } } } } recordHistoryEntry(instancesHistory); Instance rejNote = createAndStoreRejectionNote(codeService.findOrCreate(new Code(qcRejectionCode)), rejectedInstances); QCOperationContext mergeCtx = new QCContextImpl.Builder() .addSourceInstances(sourceUIDs) .addTargetInstances(targetUIDs) .addRejectionNote(rejNote) .build(); return mergeCtx; } @Override public QCOperationContext split(Enum<?> structuralChangeType, Collection<String> toMoveUIDs, IDWithIssuer pid, String targetStudyUID, Attributes createdStudyAttrs, Attributes targetSeriesAttrs, org.dcm4che3.data.Code qcRejectionCode) throws QCOperationNotPermittedException { Collection<Instance> toMove = locateInstances(toMoveUIDs); if (toMove.size() != toMoveUIDs.size()) { throw new EJBException("QC Split failed! Not all Instances to move are found!"); } if(!allInstancesFromSameStudy(toMove)) { LOG.error("{} : QC info[Split] - Failure, Different studies used as source", qcSource); throw new EJBException(); } Study sourceStudy = toMove.iterator().next().getSeries().getStudy(); checkIfQCPermittedForStudy(sourceStudy); Study targetStudy = findStudy(targetStudyUID); ActionHistory.HierarchyLevel hierarchyLevel; if (targetStudy == null) { LOG.debug("{} : QC info[Split] - Target study" + " didn't exist, creating target study",qcSource); targetStudy = createStudy(pid, sourceStudy, targetStudyUID, createdStudyAttrs); hierarchyLevel = pid == null ? ActionHistory.HierarchyLevel.STUDY : ActionHistory.HierarchyLevel.PATIENT; } else { if (createdStudyAttrs != null && createdStudyAttrs.size() > 0) { checkIfQCPermittedForStudy(targetStudy); updateStudy(archiveDeviceExtension, targetStudy, createdStudyAttrs); } hierarchyLevel = targetSeriesAttrs != null && !toMove.iterator().next().getSeries().getSeriesInstanceUID() .equals(targetSeriesAttrs.getString(Tag.SeriesInstanceUID)) ? ActionHistory.HierarchyLevel.SERIES : HierarchyLevel.STUDY; } String sourceStudyUID = sourceStudy.getStudyInstanceUID(); ActionHistory splitAction = generateQCAction(QC_OPERATION.SPLIT, hierarchyLevel); StudyHistory studyHistory = createQCStudyHistory( sourceStudyUID, targetStudy.getStudyInstanceUID(), sourceStudy.getAttributes(), splitAction); HashMap<String,NewSeriesTuple> oldToNewSeries = new HashMap<>(); List<InstanceIdentifier> sourceUIDs = new ArrayList<>(); List<InstanceIdentifier> targetUIDs = new ArrayList<>(); List<InstanceHistory> instancesHistory = new ArrayList<>(); Series newSeries = null; for (Instance instance : toMove) { Series series = instance.getSeries(); String seriesUID = series.getSeriesInstanceUID(); if (!oldToNewSeries.containsKey(seriesUID)) { if (hierarchyLevel == ActionHistory.HierarchyLevel.SERIES) { try { Query q = em.createNamedQuery(Series.FIND_BY_SERIES_INSTANCE_UID); q.setParameter(1, targetSeriesAttrs.getString(Tag.SeriesInstanceUID)); newSeries = (Series) q.getSingleResult(); } catch (NoResultException ignore) {} } if (newSeries == null) { newSeries = createSeries(series, targetStudy, targetSeriesAttrs); } SeriesHistory seriesHistory = createQCSeriesHistory(seriesUID, series.getAttributes(), studyHistory, checkNoneIOCMSource(instance)); oldToNewSeries.put(seriesUID, new NewSeriesTuple(newSeries.getPk(), seriesHistory)); } else { long newSeriesPK = oldToNewSeries.get(seriesUID).getPK(); if (newSeries.getPk() != newSeriesPK) { newSeries = em.find(Series.class, newSeriesPK); } } String instanceUID = instance.getSopInstanceUID(); Instance newInstance = move(instance, newSeries, qcRejectionCode); String newInstanceUID = newInstance.getSopInstanceUID(); InstanceHistory instanceHistory = new InstanceHistory( targetStudyUID, newSeries.getSeriesInstanceUID(), instanceUID, newInstanceUID, newInstanceUID, false); instanceHistory.setSeries(oldToNewSeries.get(seriesUID).getSeriesHistory()); instancesHistory.add(instanceHistory); targetUIDs.add(new InstanceIdentifierImpl(targetStudyUID, newSeries.getSeriesInstanceUID(), newInstanceUID)); sourceUIDs.add(new InstanceIdentifierImpl(sourceStudyUID, seriesUID, instanceUID)); } instancesHistory.addAll(handleKOPRSR(qcRejectionCode, studyHistory, sourceUIDs, targetUIDs, sourceStudy, targetStudy, oldToNewSeries)); recordHistoryEntry(instancesHistory); Instance rejNote = createAndStoreRejectionNote(qcRejectionCode, toMove); QCOperationContext splitCtx = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.SPLIT) .addSourceInstances(sourceUIDs) .addTargetInstances(targetUIDs) .addRejectionNote(rejNote) .build(); structuralChangeAggregator.aggregate(splitCtx); scheduleChangeRequestAfterTxCommit(splitCtx); return splitCtx; } @Override public QCOperationContext segment(Enum<?> structuralChangeType, Collection<String> toMoveUIDs, Collection<String> toCloneUIDs, IDWithIssuer pid, String targetStudyUID, Attributes createdStudyAttrs,Attributes targetSeriesAttrs, org.dcm4che3.data.Code qcRejectionCode) throws QCOperationNotPermittedException { //check move and clone belong to same study ArrayList<Instance> tmpAllInstancesInvolved = new ArrayList<Instance>(); Collection<Instance> toMove = locateInstances(toMoveUIDs); Collection<Instance> toClone = locateInstances(toCloneUIDs); tmpAllInstancesInvolved.addAll(toMove); tmpAllInstancesInvolved.addAll(toClone); if(!allInstancesFromSameStudy(tmpAllInstancesInvolved)) { LOG.error("{} : QC info[Segment] Failure, Different studies used as source", qcSource); throw new EJBException(); } Study sourceStudy = tmpAllInstancesInvolved.get(0).getSeries().getStudy(); checkIfQCPermittedForStudy(sourceStudy); Study targetStudy = findStudy(targetStudyUID); if(targetStudy != null) { checkIfQCPermittedForStudy(targetStudy); } if(targetStudy == null) { LOG.debug("{} : QC info[Segment] info - Target study didn't exist, creating target study",qcSource); targetStudy = createStudy(pid, sourceStudy, targetStudyUID, createdStudyAttrs); } else if (createdStudyAttrs != null && createdStudyAttrs.size() > 0) { updateStudy(archiveDeviceExtension, targetStudy, createdStudyAttrs); } if(sourceStudy.getPatient().getPk() != targetStudy.getPatient().getPk()) { LOG.error("{} : QC info[Segment] Failure, Source Study {} or Target Study {}" + " do not belong to the same patient", qcSource,sourceStudy.getStudyInstanceUID(), targetStudyUID); throw new EJBException(); } ActionHistory segmentAction = generateQCAction(QC_OPERATION.SEGMENT, ActionHistory.HierarchyLevel.STUDY); List<InstanceIdentifier> movedSourceUIDs = new ArrayList<>(); List<InstanceIdentifier> movedTargetUIDs = new ArrayList<>(); List<InstanceIdentifier> clonedSourceUIDs = new ArrayList<>(); List<InstanceIdentifier> clonedTargetUIDs = new ArrayList<>(); List<InstanceHistory> instancesHistory = new ArrayList<>(); StudyHistory studyHistory = createQCStudyHistory(sourceStudy.getStudyInstanceUID(), targetStudyUID, sourceStudy.getAttributes(), segmentAction); HashMap<String,NewSeriesTuple> oldToNewSeries = new HashMap<>(); Series newSeries; //move for(Instance instance: toMove) { String oldInstanceUID = instance.getSopInstanceUID(); String oldSeriesInstanceUID = instance.getSeries().getSeriesInstanceUID(); if(!oldToNewSeries.containsKey(oldSeriesInstanceUID)) { Series series = instance.getSeries(); newSeries= createSeries(series, targetStudy, targetSeriesAttrs); SeriesHistory seriesHistory = createQCSeriesHistory(oldSeriesInstanceUID, series.getAttributes(), studyHistory, null); oldToNewSeries.put(oldSeriesInstanceUID, new NewSeriesTuple(newSeries.getPk(), seriesHistory)); } else { newSeries = em.find(Series.class, oldToNewSeries.get(oldSeriesInstanceUID).getPK()); } String newSeriesInstanceUID = newSeries.getSeriesInstanceUID(); Instance newInstance = move(instance, newSeries, qcRejectionCode); String newInstanceUID = newInstance.getSopInstanceUID(); InstanceHistory instanceHistory = new InstanceHistory(targetStudyUID, newSeriesInstanceUID, oldInstanceUID, newInstanceUID, newInstanceUID, false); instanceHistory.setSeries(oldToNewSeries.get(oldSeriesInstanceUID).getSeriesHistory()); instancesHistory.add(instanceHistory); movedTargetUIDs.add(new InstanceIdentifierImpl(targetStudy.getStudyInstanceUID(), newSeriesInstanceUID, newInstanceUID)); movedSourceUIDs.add(new InstanceIdentifierImpl(sourceStudy.getStudyInstanceUID(), oldSeriesInstanceUID, oldInstanceUID)); } //clone for(Instance instance: toClone) { String oldInstanceUID = instance.getSopInstanceUID(); String oldSeriesInstanceUID = instance.getSeries().getSeriesInstanceUID(); if(!oldToNewSeries.containsKey(oldSeriesInstanceUID)) { Series series = instance.getSeries(); newSeries= createSeries(series, targetStudy, targetSeriesAttrs); SeriesHistory seriesHistory = createQCSeriesHistory(oldSeriesInstanceUID, series.getAttributes(), studyHistory, null); oldToNewSeries.put(oldSeriesInstanceUID, new NewSeriesTuple(newSeries.getPk(),seriesHistory)); } else { newSeries = em.find(Series.class, oldToNewSeries.get(oldSeriesInstanceUID).getPK()); } String newSeriesInstanceUID = newSeries.getSeriesInstanceUID(); Instance newInstance = clone(instance, newSeries); String newInstanceUID = newInstance.getSopInstanceUID(); InstanceHistory instanceHistory = new InstanceHistory(targetStudyUID, newSeriesInstanceUID, oldInstanceUID, newInstanceUID, newInstanceUID, true); instanceHistory.setSeries(oldToNewSeries.get(oldSeriesInstanceUID).getSeriesHistory()); instancesHistory.add(instanceHistory); clonedTargetUIDs.add(new InstanceIdentifierImpl(targetStudy.getStudyInstanceUID(), newSeriesInstanceUID, newInstanceUID)); clonedSourceUIDs.add(new InstanceIdentifierImpl(sourceStudy.getStudyInstanceUID(), oldSeriesInstanceUID, oldInstanceUID)); } instancesHistory.addAll(handleKOPRSR(qcRejectionCode, studyHistory, movedSourceUIDs, movedTargetUIDs, sourceStudy, targetStudy, oldToNewSeries)); movedSourceUIDs.addAll(clonedSourceUIDs); movedTargetUIDs.addAll(clonedTargetUIDs); recordHistoryEntry(instancesHistory); Instance rejNote = createAndStoreRejectionNote( codeService.findOrCreate(new Code(qcRejectionCode)), toMove); QCOperationContext segmentCtx = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.SEGMENT) .addSourceInstances(movedSourceUIDs) .addTargetInstances(movedTargetUIDs) .addRejectionNote(rejNote) .build(); structuralChangeAggregator.aggregate(segmentCtx); scheduleChangeRequestAfterTxCommit(segmentCtx); return segmentCtx; } @Override public QCOperationContext reject(Enum<?> structuralChangeType, String[] sopInstanceUIDs, org.dcm4che3.data.Code qcRejectionCode) throws QCOperationNotPermittedException { Collection<Instance> instances = locateInstances(sopInstanceUIDs); //TODO: Check assumption: All instances to be rejected belong to same study? if(!instances.isEmpty()) { Instance inst = instances.iterator().next(); checkIfQCPermittedForStudy(inst.getSeries().getStudy()); } rejectionService.reject(qcSource, instances, findOrCreateCode(qcRejectionCode), null); List<InstanceIdentifier> rejectedUIDs = new ArrayList<>(); for(Instance inst : instances) { rejectedUIDs.add(new InstanceIdentifierImpl(inst.getSeries().getStudy().getStudyInstanceUID(), inst.getSeries().getSeriesInstanceUID(), inst.getSopInstanceUID())); } Instance rejNote = createAndStoreRejectionNote(qcRejectionCode, instances); QCOperationContext rejectCtx = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.REJECT) .addSourceInstances(rejectedUIDs) .addRejectionNote(rejNote) .build(); structuralChangeAggregator.aggregate(rejectCtx); scheduleChangeRequestAfterTxCommit(rejectCtx); return rejectCtx; } @Override public QCOperationContext replaced(Enum<?> structuralChangeType, Map<String, String> newToOldIUIDs, org.dcm4che3.data.Code qcRejectionCode) throws QCOperationNotPermittedException { List<InstanceIdentifier> sourceUIDs = new ArrayList<>(newToOldIUIDs.size()); List<InstanceIdentifier> targetUIDs = new ArrayList<>(newToOldIUIDs.size()); List<Instance> oldInstances = new ArrayList<>(newToOldIUIDs.size()); Collection<Instance> newInstances = locateInstances(newToOldIUIDs.keySet()); Study study = null; List<InstanceHistory> instancesHistory = new ArrayList<>(); ActionHistory action = generateQCAction(QC_OPERATION.UPDATE, HierarchyLevel.INSTANCE); StudyHistory studyHistory = null; SeriesHistory seriesHistory = null; int replacedInstances = 0; for (Instance newInstance : newInstances) { if (study == null) { study = newInstance.getSeries().getStudy(); checkIfQCPermittedForStudy(study); } String newIUID = newInstance.getSopInstanceUID(); String oldIUID = newToOldIUIDs.get(newIUID); newInstance.setRejectionNoteCode(null);//unhide new Instance em.merge(newInstance); Instance oldInstance = new Instance(); Attributes attrs = new Attributes(newInstance.getAttributes()); attrs.setString(Tag.SOPInstanceUID, VR.UI, oldIUID); oldInstance.setAttributes(attrs, archiveDeviceExtension.getAttributeFilter(Entity.Instance), archiveDeviceExtension.getFuzzyStr(), archiveDeviceExtension.getNullValueForQueryFields()); oldInstance.setSeries(newInstance.getSeries()); oldInstances.add(oldInstance); String seriesIUID = newInstance.getSeries().getSeriesInstanceUID(); String studyIUID = study.getStudyInstanceUID(); if (studyHistory == null) studyHistory = createQCStudyHistory(studyIUID, studyIUID, null, action); if (seriesHistory == null) seriesHistory = createQCSeriesHistory(seriesIUID, null, studyHistory, newInstance.getSeries().getSourceAET()); InstanceHistory instanceHistory = new InstanceHistory( studyIUID, seriesIUID, oldIUID, newIUID, newIUID, false); instanceHistory.setSeries(seriesHistory); instancesHistory.add(instanceHistory); targetUIDs.add(new InstanceIdentifierImpl(studyIUID, seriesIUID, newIUID)); sourceUIDs.add(new InstanceIdentifierImpl(studyIUID, seriesIUID, oldIUID)); replacedInstances++; } if (replacedInstances != newToOldIUIDs.size()) { LOG.warn("Not all instances were replaced! -> Remaining active service entries for retry/debug!"); } HashMap<String,NewSeriesTuple> oldToNewSeries = new HashMap<String, NewSeriesTuple>(); instancesHistory.addAll(handleKOPRSR(qcRejectionCode, studyHistory, sourceUIDs, targetUIDs, study, study, oldToNewSeries)); recordHistoryEntry(instancesHistory); Instance rejNote = createAndStoreRejectionNote(qcRejectionCode, oldInstances); QCOperationContext updateCtx = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.UPDATE) .addSourceInstances(sourceUIDs) .addTargetInstances(targetUIDs) .addRejectionNote(rejNote) .build(); structuralChangeAggregator.aggregate(updateCtx); scheduleChangeRequestAfterTxCommit(updateCtx); return updateCtx; } @Override public QCOperationContext restore(Enum<?> structuralChangeType, String[] sopInstanceUIDs) throws QCOperationNotPermittedException { Collection<Instance> instances = locateInstances(sopInstanceUIDs); //TODO: Check assumption: All instances to be rejected belong to same study? if(!instances.isEmpty()) { Instance inst = instances.iterator().next(); checkIfQCPermittedForStudy(inst.getSeries().getStudy()); } List<InstanceIdentifier> restoredIUIDs = new ArrayList<>(); instances = filterQCed(instances); rejectionService.restore(qcSource, instances, null); for(Instance inst : instances) { restoredIUIDs.add(new InstanceIdentifierImpl( inst.getSeries().getStudy().getStudyInstanceUID(), inst.getSeries().getSeriesInstanceUID(), inst.getSopInstanceUID())); } QCOperationContext restoreCtx = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.RESTORE) .addSourceInstances(restoredIUIDs) .build(); structuralChangeAggregator.aggregate(restoreCtx); //TODO: schedule change request for restored instances (= send restore note)? return restoreCtx; } @Override public boolean canApplyQC(Instance instance) { return instance.getRejectionNoteCode() == null; } @Override public QCOperationContext updateDicomObject(Enum<?> structuralChangeType, ArchiveDeviceExtension arcDevExt, UpdateScope scope, Attributes attrs) throws QCOperationNotPermittedException, EntityNotFoundException { ActionHistory updateAction = generateQCAction(QC_OPERATION.UPDATE, ActionHistory.HierarchyLevel.INSTANCE); LOG.info("{}: QC info[Update] info - Performing QC update DICOM header on {} scope : ", qcSource, scope); Attributes unmodified; PatientAttrsPKTuple unmodifiedAndPK = null; String queryString = null, queryParam = null; switch(scope) { case PATIENT: unmodifiedAndPK=updatePatient(arcDevExt, attrs); unmodified = unmodifiedAndPK.getUnModifiedAttrs(); break; case STUDY: queryString = "SELECT i.sopInstanceUID from Instance i WHERE i.series.study.studyInstanceUID = ?1"; queryParam = attrs.getString(Tag.StudyInstanceUID); unmodified = updateStudy(arcDevExt, queryParam, attrs); break; case SERIES: queryString = "SELECT i.sopInstanceUID from Instance i WHERE i.series.seriesInstanceUID = ?1"; queryParam = attrs.getString(Tag.SeriesInstanceUID); unmodified = updateSeries(arcDevExt, queryParam, attrs); break; case INSTANCE: queryString = "SELECT i.sopInstanceUID from Instance i WHERE i.sopInstanceUID = ?1"; queryParam = attrs.getString(Tag.SOPInstanceUID); unmodified = updateInstance(arcDevExt, queryParam, attrs); break; default : LOG.error("{} : QC info[Update] Failure - invalid update scope", qcSource); throw new EJBException(); } LOG.info("{} : QC info[Update] info - Update successful, adding update history entry", qcSource); addUpdateHistoryEntry(updateAction, scope, unmodified, scope == UpdateHistory.UpdateScope.PATIENT ? Long.toString(unmodifiedAndPK.getPK()) : null); List<InstanceIdentifier> updatedIUIDs = new ArrayList<>(); if (queryString != null) { TypedQuery<String> query = em.createQuery(queryString, String.class); query.setParameter(1, queryParam); for (String updatedUID : query.getResultList()) { updatedIUIDs.add(new InstanceIdentifierImpl(null, null, updatedUID)); } } QCOperationContext updateCtx = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.UPDATE, scope) .addSourceInstances(updatedIUIDs) .addTargetInstances(updatedIUIDs) .updateAttributes(attrs) .build(); structuralChangeAggregator.aggregate(updateCtx); scheduleChangeRequestAfterTxCommit(updateCtx); return updateCtx; } public boolean patientOperation(Attributes srcPatientAttrs, Attributes targetPatientAttrs, ArchiveAEExtension arcAEExt, PatientCommands command) { try { if(command == PatientCommands.PATIENT_UPDATE_ID) patientService.updatePatientID(srcPatientAttrs, targetPatientAttrs,arcAEExt.getStoreParam()); if (command == PatientCommands.PATIENT_LINK) patientService.linkPatient(targetPatientAttrs, srcPatientAttrs, arcAEExt.getStoreParam()); else if (command == PatientCommands.PATIENT_UNLINK) patientService.unlinkPatient(targetPatientAttrs, srcPatientAttrs, arcAEExt.getStoreParam()); else if (command == PatientCommands.PATIENT_MERGE) { patientService.mergePatientByHL7( targetPatientAttrs, srcPatientAttrs, arcAEExt.getStoreParam()); } return true; } catch(Exception e) { LOG.error("{} : QC info[Patient Operation] Failure - Patient operation " + "failed, reason {}", qcSource, e); return false; } } @Override public QCOperationContext deletePatient(Enum<?> structuralChangeType, IDWithIssuer pid, org.dcm4che3.data.Code qcRejectionCode) throws QCOperationNotPermittedException { List<InstanceIdentifier> deletedUIDs = new ArrayList<>(); Collection<Instance> rejectedInstances = new ArrayList<>(); Patient patient = findPatient(pid); Collection<Study> studies = patient.getStudies(); for(Study study : studies) { checkIfQCPermittedForStudy(study); } for(Study study : studies) { Collection<Series> allSeries = study.getSeries(); for(Series series: allSeries) { Collection<Instance> insts = series.getInstances(); for(Instance inst : insts) { deletedUIDs.add(new InstanceIdentifierImpl(study.getStudyInstanceUID(), series.getSeriesInstanceUID(), inst.getSopInstanceUID())); } rejectedInstances.addAll(insts); } } LOG.info("{}: QC info[Delete] info - Rejected patient instances {} " + "- scheduled to delete",qcSource , pid.toString()); // TODO: BUG-ALARM: Should there be one rejection note per deleted study?! Instance rejNote = createAndStoreRejectionNote(qcRejectionCode, rejectedInstances); QCOperationContext deleteCtx = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.DELETE) .addSourceInstances(deletedUIDs) .addRejectionNote(rejNote) .build(); structuralChangeAggregator.aggregate(deleteCtx); scheduleChangeRequestAfterTxCommit(deleteCtx); createQCDeleteHistory(rejectedInstances); rejectAndScheduleForDeletion(rejectedInstances, qcRejectionCode); return deleteCtx; } @Override public QCOperationContext deleteStudy(Enum<?> structuralChangeType, String studyInstanceUID, org.dcm4che3.data.Code qcRejectionCode) throws QCOperationNotPermittedException { List<InstanceIdentifier> deletedUIDs = new ArrayList<>(); Collection<Instance> rejectedInstances = new ArrayList<>(); TypedQuery<Study> query = em.createNamedQuery( Study.FIND_BY_STUDY_INSTANCE_UID, Study.class) .setParameter(1, studyInstanceUID); Study study = query.getSingleResult(); checkIfQCPermittedForStudy(study); Collection<Series> allSeries = study.getSeries(); for(Series series: allSeries) { Collection<Instance> insts = series.getInstances(); for(Instance inst : insts) { deletedUIDs.add(new InstanceIdentifierImpl(study.getStudyInstanceUID(), series.getSeriesInstanceUID(), inst.getSopInstanceUID())); } rejectedInstances.addAll(insts); } LOG.info("{}: QC info[Delete] info - Rejected study instances {} " + "- scheduled to delete",qcSource , studyInstanceUID); Instance rejNote = createAndStoreRejectionNote(qcRejectionCode, rejectedInstances); QCOperationContext deleteCtx = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.DELETE) .addSourceInstances(deletedUIDs) .addRejectionNote(rejNote) .build(); structuralChangeAggregator.aggregate(deleteCtx); scheduleChangeRequestAfterTxCommit(deleteCtx); createQCDeleteHistory(rejectedInstances); rejectAndScheduleForDeletion(rejectedInstances, qcRejectionCode); study.setRejected(true); return deleteCtx; } @Override public QCOperationContext deleteSeries(Enum<?> structuralChangeType, String seriesInstanceUID, org.dcm4che3.data.Code qcRejectionCode) throws QCOperationNotPermittedException { List<InstanceIdentifier> deletedUIDs = new ArrayList<>(); TypedQuery<Series> query = em.createNamedQuery( Series.FIND_BY_SERIES_INSTANCE_UID, Series.class) .setParameter(1, seriesInstanceUID); Series series = query.getSingleResult(); Collection<Instance> insts = series.getInstances(); Study study = series.getStudy(); checkIfQCPermittedForStudy(study); for(Instance inst : insts) { deletedUIDs.add(new InstanceIdentifierImpl(study.getStudyInstanceUID(), series.getSeriesInstanceUID(), inst.getSopInstanceUID())); } em.createNamedQuery(StudyQueryAttributes.CLEAN_FOR_STUDY).setParameter(1, study.getPk()).executeUpdate(); LOG.info("{}: QC info[Delete] info - Rejected series instances {} " + "- scheduled for delete",qcSource, seriesInstanceUID); Instance rejNote = createAndStoreRejectionNote(qcRejectionCode, insts); QCOperationContext deleteCtx = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.DELETE) .addSourceInstances(deletedUIDs) .addRejectionNote(rejNote) .build(); structuralChangeAggregator.aggregate(deleteCtx); scheduleChangeRequestAfterTxCommit(deleteCtx); createQCDeleteHistory(insts); rejectAndScheduleForDeletion(insts, qcRejectionCode); series.setRejected(true); return deleteCtx; } @Override public QCOperationContext deleteInstance(Enum<?> structuralChangeType, String sopInstanceUID, org.dcm4che3.data.Code qcRejectionCode) throws QCOperationNotPermittedException { Collection<Instance> deletedInstances = locateInstances(sopInstanceUID); if(deletedInstances.isEmpty()) { LOG.debug("{}: QC info[Delete] Failure - Error finding " + "instance to delete with SOPInstanceUID={}", qcSource,sopInstanceUID); throw new EJBException(); } Series series = deletedInstances.iterator().next().getSeries(); Study study = series.getStudy(); checkIfQCPermittedForStudy(study); LOG.info("{}: QC info[Delete] info - Rejected instance {} - scheduled for delete", qcSource, sopInstanceUID); em.createNamedQuery(SeriesQueryAttributes.CLEAN_FOR_SERIES).setParameter(1, series.getPk()).executeUpdate(); em.createNamedQuery(StudyQueryAttributes.CLEAN_FOR_STUDY).setParameter(1, study.getPk()).executeUpdate(); List<InstanceIdentifier> deletedUIDs = new ArrayList<>(); for(Instance inst : deletedInstances) { deletedUIDs.add(new InstanceIdentifierImpl(study.getStudyInstanceUID(),series.getSeriesInstanceUID(), inst.getSopInstanceUID())); } Instance rejNote = createAndStoreRejectionNote(qcRejectionCode, deletedInstances); QCOperationContext deleteCtx = new QCContextImpl.Builder(structuralChangeType, QC_OPERATION.DELETE) .addSourceInstances(deletedUIDs) .addRejectionNote(rejNote) .build(); structuralChangeAggregator.aggregate(deleteCtx); scheduleChangeRequestAfterTxCommit(deleteCtx); createQCDeleteHistory(deletedInstances); rejectAndScheduleForDeletion(deletedInstances, qcRejectionCode); return deleteCtx; } private String checkNoneIOCMSource(Instance inst) { NoneIOCMChangeRequestorExtension ext = device.getDeviceExtension(NoneIOCMChangeRequestorExtension.class); if (ext != null) { String srcAET = inst.getSeries().getSourceAET(); for(Device dev : ext.getNoneIOCMModalityDevices()) { if(dev.getApplicationAETitles().contains(srcAET)) { return srcAET; } } for(Device dev : ext.getNoneIOCMChangeRequestorDevices()) { if(dev.getApplicationAETitles().contains(srcAET)) { return srcAET; } } } return null; } @Override public boolean deleteSeriesIfEmpty(String seriesInstanceUID, String studyInstanceUID) { TypedQuery<Series> query = em.createNamedQuery(Series.FIND_BY_SERIES_INSTANCE_UID, Series.class) .setParameter(1, seriesInstanceUID); Series series = query.getSingleResult(); if(series.getInstances().isEmpty()) { em.remove(series); LOG.info("{}: QC info[Delete] info - Removed series entity {}", qcSource, seriesInstanceUID); return true; } return false; } @Override public boolean deletePatientIfEmpty(IDWithIssuer pid) { Patient patient = findPatient(pid); if(patient == null) return false; if (patient.getStudies().isEmpty()) { em.remove(patient); LOG.info("{}: QC info[Delete] info - Removed patient entity {}", qcSource, pid); return true; } return false; } @Override public boolean deleteStudyIfEmpty(String studyInstanceUID) { TypedQuery<Study> query = em.createNamedQuery( Study.FIND_BY_STUDY_INSTANCE_UID, Study.class).setParameter(1, studyInstanceUID); Study study = query.getSingleResult(); if (study.getSeries().isEmpty()) { em.remove(study); LOG.info("{}: QC info[Delete] info - Removed study entity {}", qcSource, studyInstanceUID); return true; } return false; } @Override public Collection<Instance> locateInstances(String... sopInstanceUIDs) { if(sopInstanceUIDs == null) { LOG.error("{} : QC info[locateInstance] - Unable to locate instances with null UIDs" + " , returning an empty list", qcSource); return Collections.emptyList(); } return locateInstances(Arrays.asList(sopInstanceUIDs)); } private Collection<Instance> locateInstances(Collection<String> sopIUIDs) { if (sopIUIDs.isEmpty()) { return Collections.emptyList(); } return em.createNamedQuery(Instance.FIND_BY_SOP_INSTANCE_UID_EAGER_MANY, Instance.class) .setParameter("uids", sopIUIDs) .getResultList(); } private void createQCDeleteHistory(Collection<Instance> rejectedInstances) { ActionHistory deleteAction = generateQCAction(QC_OPERATION.DELETE, ActionHistory.HierarchyLevel.INSTANCE); Map<String, StudyHistory> studiesInHistory = new HashMap<String, StudyHistory>(); Map<String, SeriesHistory> seriesInHistory = new HashMap<String, SeriesHistory>(); Collection<InstanceHistory> instancesInHistory = new ArrayList<InstanceHistory>(); String studyIUID, seriesIUID, sopIUID; for(Instance inst : rejectedInstances) { sopIUID = inst.getSopInstanceUID(); seriesIUID = inst.getSeries().getSeriesInstanceUID(); studyIUID = inst.getSeries().getStudy().getStudyInstanceUID(); if(!studiesInHistory.containsKey(studyIUID)) { StudyHistory studyHistory = createQCStudyHistory( studyIUID, studyIUID, inst.getSeries().getStudy().getAttributes(), deleteAction); studiesInHistory.put(studyIUID, studyHistory); } SeriesHistory seriesHistory = seriesInHistory.get(seriesIUID); if(seriesHistory == null) { seriesHistory = createQCSeriesHistory( seriesIUID, inst.getSeries().getAttributes(), studiesInHistory.get(studyIUID), checkNoneIOCMSource(inst)); seriesInHistory.put(seriesIUID, seriesHistory); } InstanceHistory instanceHistory = new InstanceHistory( studyIUID, seriesIUID, sopIUID, sopIUID, sopIUID, false); instanceHistory.setSeries(seriesHistory); instancesInHistory.add(instanceHistory); } recordHistoryEntry(instancesInHistory); } private Instance move(Instance sourceInstance, Series targetSeries, org.dcm4che3.data.Code qcRejectionCode) { if(!canApplyQC(sourceInstance)) { LOG.error("{} : QC info[move] Failure - Can't apply QC operation on already QCed" + " or rejected object",qcSource); throw new EJBException(); } reject(Arrays.asList(sourceInstance), qcRejectionCode); try { return createNewInstance(sourceInstance, targetSeries); } catch (Exception e) { LOG.error("{} : QC info[move] Failure - Unable to" + " create replacement instance for {}," + " rolling back move", qcSource,sourceInstance); throw new EJBException(e); } } private Instance clone(Instance source, Series target) { if(!canApplyQC(source)) { LOG.error("{} : QC info[clone] Can't apply QC operation on already QCed" + " or rejected object",qcSource); throw new EJBException(); } try { return createNewInstance(source, target); } catch (Exception e) { LOG.error("{} : QC info[clone] Unable to create cloned instance for {}," + " rolling back clone", qcSource,source); throw new EJBException(e); } } private Instance createNewInstance(Instance source, Series target) throws DicomServiceException { Instance newInstance = createInstance(source, target); for (Location location : source.getLocations()) { location.addInstance(newInstance); } return newInstance; } private void reject(Collection<Instance> instances, org.dcm4che3.data.Code qcRejectionCode) { //ActionHistory rejectAction = generateQCAction(QCOperation.REJECT); ArrayList<String> sopInstanceUIDs = new ArrayList<String>(); try { rejectionService.reject(qcSource, instances, findOrCreateCode(qcRejectionCode), null); for(Instance inst: instances) { sopInstanceUIDs.add(inst.getSopInstanceUID()); } //createRejectHistory(instances, rejectAction); } catch(Exception e) { LOG.error("{} : QC info[reject] Failure - Reject Failure, reason {}", qcSource, e); throw new EJBException(); } } private void rejectAndScheduleForDeletion(Collection<Instance> insts, org.dcm4che3.data.Code qcRejectionCode) { rejectionService.reject(this, insts, codeService.findOrCreate(qcRejectionCode), null); rejectionServiceDeleter.deleteRejected(this, insts); } /** * Creates the QC series history entity. * Sets the history attributes to that of the old series. * * @param seriesInstanceUID * the series instance uid * @param oldAttributes * the old attributes * @param studyHistory * the study history * @return the QC series history */ @Override public SeriesHistory createQCSeriesHistory(String seriesInstanceUID, Attributes oldAttributes, StudyHistory studyHistory, String noneIocmSourceAET) { SeriesHistory seriesHistory = new SeriesHistory(); seriesHistory.setStudy(studyHistory); if(oldAttributes != null && !oldAttributes.isEmpty()) seriesHistory.setUpdatedAttributesBlob(new AttributesBlob(oldAttributes)); seriesHistory.setOldSeriesUID(seriesInstanceUID); seriesHistory.setNoneIOCMSourceAET(noneIocmSourceAET); em.persist(seriesHistory); return seriesHistory; } /** * Creates the QC study history entity. * Sets the history attributes to that of the old study. * * @param studyInstanceUID * the study instance uid * @param target * @param oldAttributes * the old attributes * @param actionHistory * the qc action history * @return the QC study history */ @Override public StudyHistory createQCStudyHistory(String studyInstanceUID, String targetStudyUID, Attributes oldAttributes, ActionHistory actionHistory) { StudyHistory studyHistory = new StudyHistory(); studyHistory.setAction(actionHistory); if(oldAttributes != null && !oldAttributes.isEmpty()) studyHistory.setUpdatedAttributesBlob(new AttributesBlob(oldAttributes)); studyHistory.setOldStudyUID(studyInstanceUID); studyHistory.setNextStudyUID(targetStudyUID); em.persist(studyHistory); return studyHistory; } /** * Update patient. * Updates the attributes of the patient to the provided attributes * Throws exception on patient not found. * * @param arcDevExt * the arc dev ext * @param attrs * the attrs * @param patientPK * @return the attributes * @throws EntityNotFoundException * the entity not found exception */ private PatientAttrsPKTuple updatePatient(ArchiveDeviceExtension arcDevExt, Attributes attrs) throws EntityNotFoundException { Patient patient = findPatient(attrs); if (patient == null) throw new EntityNotFoundException( "Unable to find patient or multiple patients found"); Attributes original = patient.getAttributes(); Attributes unmodified = new Attributes(); unmodified.addAll(patient.getAttributes()); original.update(attrs, original); if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[Update] info - " + "Attributes modified:\n {}",qcSource, attrs.toString()); } patient.setAttributes(original, arcDevExt.getAttributeFilter(Entity.Patient), arcDevExt.getFuzzyStr(), arcDevExt.getNullValueForQueryFields()); return new PatientAttrsPKTuple(patient.getPk(), unmodified); } /** * Update study. * Updates the attributes of a study * Throws exception on study not found. * * @param arcDevExt * the arc dev ext * @param studyInstanceUID * the study instance uid * @param attrs * the attrs * @return the attributes * @throws EntityNotFoundException * the entity not found exception */ private Attributes updateStudy(ArchiveDeviceExtension arcDevExt, String studyInstanceUID, Attributes attrs) throws QCOperationNotPermittedException, EntityNotFoundException { Study study = findStudy(studyInstanceUID); if (study == null) throw new EntityNotFoundException( "Unable to find study "+ studyInstanceUID); return updateStudy(arcDevExt, study, attrs); } private Attributes updateStudy(ArchiveDeviceExtension arcDevExt, Study study, Attributes attrs) { Attributes original = study.getAttributes(); Attributes unmodified = new Attributes(study.getAttributes()); //check StudyIUID if (attrs.contains(Tag.StudyInstanceUID)) { if (!study.getStudyInstanceUID().equals(attrs.getString(Tag.StudyInstanceUID))) { LOG.warn("updateStudy would override Study Instance UID {} with {}!", study.getStudyInstanceUID(), attrs.getString(Tag.StudyInstanceUID)); throw new IllegalArgumentException("StudyInstanceUID of study attributes do not match with study!"); } } // relations if (attrs.contains(Tag.ProcedureCodeSequence)) { Collection<Code> procedureCodes = findProcedureCodes(attrs); if (!procedureCodes.isEmpty()) { study.setProcedureCodes(procedureCodes); } } // one item only if (attrs.contains(Tag.IssuerOfAccessionNumberSequence)) { Issuer issuerOfAccessionNumber = findIssuerOfAccessionNumber(attrs); if (issuerOfAccessionNumber != null) { study.setIssuerOfAccessionNumber(issuerOfAccessionNumber); } } if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[Update] info - " + "Attributes modified:\n" + attrs.toString()); } original.update(attrs, original); study.setAttributes(original, arcDevExt.getAttributeFilter(Entity.Study), arcDevExt.getFuzzyStr(), arcDevExt.getNullValueForQueryFields()); return unmodified; } /** * Update series. * Updates the attributes of a series * Throws exception on series not found. * * @param arcDevExt * the arc dev ext * @param seriesInstanceUID * the series instance uid * @param attrs * the attrs * @return the attributes * @throws EntityNotFoundException * the entity not found exception */ private Attributes updateSeries(ArchiveDeviceExtension arcDevExt, String seriesInstanceUID, Attributes attrs) throws QCOperationNotPermittedException, EntityNotFoundException { Series series = findSeries(seriesInstanceUID); if (series == null) { throw new EntityNotFoundException("Unable to find series " + seriesInstanceUID); } checkIfQCPermittedForStudy(series.getStudy()); Attributes original = series.getAttributes(); Attributes unmodified = new Attributes(); unmodified.addAll(series.getAttributes()); // relations // institutionCode if (attrs.contains(Tag.InstitutionCodeSequence)) { Code institutionCode = findInstitutionalCode(attrs); if (institutionCode != null) { series.setInstitutionCode(institutionCode); } } // Requested Procedure Step if (attrs.contains(Tag.RequestAttributesSequence)) { Collection<RequestAttributes> requestAttrs = findRequestAttributes( series, attrs, arcDevExt, original); series.setRequestAttributes(requestAttrs); } if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[Update] info - " + "Attributes modified:\n" + attrs.toString()); } original.update(attrs, original); series.setAttributes(original, arcDevExt.getAttributeFilter(Entity.Series), arcDevExt.getFuzzyStr(), arcDevExt.getNullValueForQueryFields()); return unmodified; } /** * Update instance. * Updates the attributes of an instance. * Throws exception on instance not found. * * @param arcDevExt * the arc dev ext * @param sopInstanceUID * the sop instance uid * @param attrs * the attrs * @return the attributes * @throws EntityNotFoundException * the entity not found exception */ private Attributes updateInstance(ArchiveDeviceExtension arcDevExt, String sopInstanceUID, Attributes attrs) throws QCOperationNotPermittedException, EntityNotFoundException { Instance instance = findInstance(sopInstanceUID); if (instance == null) { throw new EntityNotFoundException("Unable to find instance " + sopInstanceUID); } checkIfQCPermittedForStudy(instance.getSeries().getStudy()); Attributes original = instance.getAttributes(); Attributes unmodified = new Attributes(); unmodified.addAll(instance.getAttributes()); // relations // Concept name Code Sequence on root level (SR) if (attrs.contains(Tag.ConceptNameCodeSequence)) { Code conceptNameCode = findConceptNameCode(attrs); if (conceptNameCode != null) { instance.setConceptNameCode(conceptNameCode); } } // verifying observers if (attrs.contains(Tag.VerifyingObserverSequence)) { Collection<VerifyingObserver> newObservers = findVerifyingObservers( instance, attrs, original, arcDevExt); if (newObservers != null) { instance.setVerifyingObservers(newObservers); } } if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[Update] info - " + "Attributes modified:\n" + attrs.toString()); } original.update(attrs, original); instance.setAttributes(original, arcDevExt.getAttributeFilter(Entity.Instance), arcDevExt.getFuzzyStr(), arcDevExt.getNullValueForQueryFields()); return unmodified; } @Override public Patient findPatient(Attributes attrs) { Collection<IDWithIssuer> pids = IDWithIssuer.pidsOf(attrs); Collection<PatientID> queryIDs = findPatientIDs(pids); if(queryIDs.isEmpty()) return null; Iterator<PatientID> idIterator = queryIDs.iterator(); Patient pat = idIterator.next().getPatient(); while(idIterator.hasNext()) { if(idIterator.next().getPatient().getPk() != pat.getPk()) { return null; } } if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[findPatient] info - " + "Found patient {}", qcSource, pat.toString()); } return pat; } /** * Gets the study from the archive. * * @param studyInstanceUID * the study instance UID * @return the study */ private Study findStudy(String studyInstanceUID) { try{ Query query = em.createNamedQuery(Study.FIND_BY_STUDY_INSTANCE_UID_EAGER); query.setParameter(1, studyInstanceUID); Study study = (Study) query.getSingleResult(); return study; } catch(NoResultException e) { if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[findStudy] Failure - " + "Unable to find study {}", qcSource, studyInstanceUID); } return null; } } @Override public List<StudyHistory> findStudyHistory(String oldStudyInstanceUID) { try{ Query query = em.createNamedQuery(StudyHistory.FIND_BY_OLD_STUDY_UID); query.setParameter(1, oldStudyInstanceUID); List<StudyHistory> resultList = (List<StudyHistory>) query.getResultList(); return resultList; } catch(NoResultException e) { if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[findStudyHistory] Failure - " + "Unable to find studyHistory {}", qcSource, oldStudyInstanceUID); } return null; } } /** * Gets the series from the archive. * * @param seriesInstanceUID * the series instance UID * @return the series */ private Series findSeries(String seriesInstanceUID) { Query query = em.createNamedQuery(Series.FIND_BY_SERIES_INSTANCE_UID_EAGER); query.setParameter(1, seriesInstanceUID); Series series = null; try{ series = (Series) query.getSingleResult(); } catch(NoResultException e) { LOG.error("{} : QC info[findSeries] error - Failed to find series " + "- reason {}", qcSource, e); } if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[findSeries] info - " + "Found series {}", qcSource, series.toString()); } return series; } /** * Gets an instance of from the archive. * * @param sopInstanceUID * the SOP instance UID of the instance * @return single instance of QCBeanImpl */ private Instance findInstance(String sopInstanceUID) { Query query = em.createNamedQuery(Instance.FIND_BY_SOP_INSTANCE_UID_EAGER); query.setParameter(1, sopInstanceUID); Instance instance = (Instance) query.getSingleResult(); if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[findInstance] info - " + "Found instance {}", qcSource, instance.toString()); } return instance; } /** * Gets the procedure codes associated with a study. * * @param attrs * the study attributes containing the * ProcedureCodeSequence * @return the procedure codes */ private Collection<Code> findProcedureCodes(Attributes attrs) { ArrayList<Code> resultCode = new ArrayList<Code>(); for (Attributes codeAttrs : attrs .getSequence(Tag.ProcedureCodeSequence)) { Code code = codeService.findOrCreate(new Code(codeAttrs)); resultCode.add(code); } return resultCode; } /** * Gets the concept name code associated with an instance. * * @param attrs * the attributes of the instance containing the * ConceptNameCodeSequence * @return the concept name code */ private Code findConceptNameCode(Attributes attrs) { Attributes codeAttrs = attrs.getNestedDataset(Tag.ConceptNameCodeSequence); Code code = null; if (codeAttrs != null) try { code = codeService.findOrCreate(new Code(codeAttrs)); } catch (Exception e) { LOG.info("Illegal code item in Sequence {}:\n{}", TagUtils.toString(Tag.ConceptNameCodeSequence), codeAttrs); } return code; } /** * Gets the institutional code for a series. * * @param attrs * the attributes of the series containing the * InstitutionCodeSequence * @return the institutional code */ private Code findInstitutionalCode(Attributes attrs) { Attributes codeAttrs = attrs.getSequence(Tag.InstitutionCodeSequence).get(0); Code code = codeService.findOrCreate(new Code(codeAttrs)); return code; } /** * Gets the issuer of accession number for a study. * * @param attrs * the attributes of the study containing * IssuerOfAccessionNumberSequence * @return the issuer of accession number */ private Issuer findIssuerOfAccessionNumber(Attributes attrs) { Attributes issuerAttrs = null; if(attrs.contains(Tag.IssuerOfAccessionNumberSequence)) issuerAttrs= attrs.getSequence(Tag.IssuerOfAccessionNumberSequence).get(0); if(issuerAttrs == null) return null; Issuer issuer = issuerService.findOrCreate(new Issuer(issuerAttrs)); return issuer; } /** * Gets the request attributes for a series. * The method will remove any extra request attributes * in the series that are not in the provided attributes. * the method will also add any missing request attributes * from the series that are present in the attributes. * The method updates the series and returns the * new associated request attributes collection. * * @param series * the series * @param attrs * the attribte * @param arcDevExt * the arc dev ext * @param original * the original * @return the request attributes */ private Collection<RequestAttributes> findRequestAttributes(Series series, Attributes attrs, ArchiveDeviceExtension arcDevExt, Attributes original) { Collection<RequestAttributes> oldRequests = series .getRequestAttributes(); Sequence oldSequence = original .getSequence(Tag.RequestAttributesSequence); Sequence updateSequence = attrs .getSequence(Tag.RequestAttributesSequence); //avoid null pointer on series with no request attributes if(updateSequence == null) { return series.getRequestAttributes(); } // remove deprecated items if (oldSequence != null) for (Attributes oldItem : oldSequence) { if (!updateSequence.contains(oldItem)) { RequestAttributes tmp = findRequestAttr(oldItem, series); if(tmp!=null) oldRequests.remove(tmp); if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[findRequestAttributes] info - " + "Removing Deprecated request attribute" + " {}", qcSource, tmp.toString()); } } } // add missing ones for (Attributes request : updateSequence) { if (oldSequence == null || (oldSequence != null && !oldSequence.contains(request))) { Issuer issuerOfAccessionNumber = findIssuerOfAccessionNumber(request); RequestAttributes newRequest = new RequestAttributes(request, issuerOfAccessionNumber, arcDevExt.getFuzzyStr(), arcDevExt.getNullValueForQueryFields()); newRequest.setSeries(series); oldRequests.add(newRequest); if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[findRequestAttributes] info - " + "Adding new request attribute" + " {}", qcSource, newRequest.toString()); } } } return oldRequests; } /** * Gets the request attribute. * Helper method that takes one request attribute * and looks it up in the database * @param attrs * the attributes * @param series * the series * @return the request attr */ private RequestAttributes findRequestAttr(Attributes attrs, Series series) { Query query = em.createQuery("SELECT r FROM RequestAttributes r " + "WHERE r.scheduledProcedureStepID = ?1 and " + "r.requestedProcedureID = ?2 and " + "r.series = ?3"); query.setParameter(1, attrs.getString(Tag.ScheduledProcedureStepID)); query.setParameter(2, attrs.getString(Tag.RequestedProcedureID)); query.setParameter(3, series); RequestAttributes request = null; try { request = (RequestAttributes) query.getSingleResult(); } catch (NoResultException e) { LOG.debug("No old request attributes found! new attributes {} will be used - reason {}", attrs, e); } return request; } /** * Gets the verifying observers from the archive. * The method retrieves the verifying observers associated * with an instance in the archive. * updates the associated verifying observers with those found * in the attributes and associates them with the instance. * The method then returns the new (updated) observers. * * @param instance * the instance * @param attrs * the attrs * @param original * the original * @param arcDevExt * the arc dev ext * @return the verifying observers */ private Collection<VerifyingObserver> findVerifyingObservers( Instance instance, Attributes attrs, Attributes original, ArchiveDeviceExtension arcDevExt) { Collection<VerifyingObserver> oldObservers = instance .getVerifyingObservers(); Sequence verifyingObserversOld = original .getSequence(Tag.VerifyingObserverSequence); Sequence verifyingObserversNew = attrs .getSequence(Tag.VerifyingObserverSequence); // remove deprecated observers if (verifyingObserversOld != null) for (Attributes observer : verifyingObserversOld) { if (!verifyingObserversNew.contains(observer)) { VerifyingObserver tmp = findVerifyingObserver(observer, instance, arcDevExt); oldObservers.remove(tmp); if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[findVerifyingObservers] info - " + "Removing deprecated verifying observer" + " {}", qcSource, tmp.getVerifyingObserverName()); } } } // add missing ones for (Attributes observer : verifyingObserversNew) { if (verifyingObserversOld == null || (verifyingObserversOld != null && !verifyingObserversOld .contains(observer))) { VerifyingObserver newObserver = new VerifyingObserver(observer, arcDevExt.getFuzzyStr(), arcDevExt.getNullValueForQueryFields()); newObserver.setInstance(instance); oldObservers.add(newObserver); if (LOG.isDebugEnabled()) { LOG.debug("{} : QC info[findVerifyingObservers] info - " + "Adding new verifying observer" + " {}", qcSource, newObserver.getVerifyingObserverName()); } } } return oldObservers; } /** * Gets the verifying observer. * Helper method that takes one observer and * looks it up in the archive database. * * @param observerAttrs * the observer attrs * @param instance * the instance * @param arcDevExt * the arc dev ext * @return the verifying observer */ private VerifyingObserver findVerifyingObserver(Attributes observerAttrs, Instance instance, ArchiveDeviceExtension arcDevExt) { Query query = em.createQuery("Select o from VerifyingObserver o " + "where o.instance = ?1"); query.setParameter(1, instance); VerifyingObserver foundObserver = (VerifyingObserver) query.getSingleResult(); return foundObserver; } /** * Find or create patientIDs. * The method looks for patient IDs in the database * if found they are returned else returns null * * @param ids * the ids * @return the collection */ private Collection<PatientID> findPatientIDs(Collection<IDWithIssuer> ids) { ArrayList<PatientID> foundIDs = new ArrayList<PatientID>(); for(IDWithIssuer id : ids){ Issuer issuer = issuerService.findOrCreate(new Issuer(id.getIssuer())); Query query = em.createQuery( "Select pid from PatientID pid where pid.id = ?1 AND pid.issuer = ?2"); query.setParameter(1, id.getID()); query.setParameter(2, issuer); PatientID foundID = (PatientID) query.getSingleResult(); foundID.setIssuer(issuer); foundIDs.add(foundID); } return foundIDs; } /** * Find or create code. * Uses the archive code service to find persisted codes or * to create new ones in case they can't be found in the archive. * * @param code * the code * @return the code found or created */ private Code findOrCreateCode(org.dcm4che3.data.Code code) { return codeService.findOrCreate(new Code(code)); } /** * Gets the same source study. * returns a study if all the provided instances belong to the * same sudy, returns null otherwise. * * @param toMove * the to move * @return the same source study (if applicable) */ private boolean allInstancesFromSameStudy(Collection<Instance> toMove) { Study study = null; for(Instance instance: toMove) { if(study==null) study=instance.getSeries().getStudy(); Study currentStudy = instance.getSeries().getStudy(); if(!currentStudy.getStudyInstanceUID() .equals(study.getStudyInstanceUID())) return false; } return true; } /** * Gets the patient. * Gets the patient associated with the provided patient ID. * * @param pid * the pid * @return the patient */ private Patient findPatient(IDWithIssuer pid) { Collection<IDWithIssuer> pids = new ArrayList<IDWithIssuer>(); pids.add(pid); Collection<PatientID> queryIDs = findPatientIDs(pids); if(queryIDs.isEmpty()) return null; Iterator<PatientID> idIterator = queryIDs.iterator(); return idIterator.next().getPatient(); } /** * Creates a new study. * The method creates a new study while copying all data from the * old study and using it to initialize the newly created study. * the method also updates the new study with the provided attributes * if the attributes are not empty. * The method will throw an EJBException if the patient doesn't exist. * * @param pid * the pid * @param targetStudyUID * the target study uid * @param createdStudyAttrs * the created study attrs * @return the study created */ private Study createStudy(IDWithIssuer pid, Study sourceStudy, String targetStudyUID, Attributes createdStudyAttrs) { Study study = new Study(); Attributes studyAttrs = new Attributes(sourceStudy.getAttributes()); if ( createdStudyAttrs != null && createdStudyAttrs.size() > 0) studyAttrs.addAll(createdStudyAttrs); Patient patient = pid == null ? sourceStudy.getPatient() : findPatient(pid); if(patient == null) { LOG.error("{} : QC info[createStudy] Failure - Study " + "Creation failed, Patient not found",qcSource); throw new EJBException(); } study.setPatient(patient); if (studyAttrs.contains(Tag.ProcedureCodeSequence)) { Collection<Code> procedureCodes = findProcedureCodes(studyAttrs); if (!procedureCodes.isEmpty()) { study.setProcedureCodes(procedureCodes); } } if (studyAttrs.contains(Tag.IssuerOfAccessionNumberSequence)) { Issuer issuerOfAccessionNumber = findIssuerOfAccessionNumber(studyAttrs); if (issuerOfAccessionNumber != null) { study.setIssuerOfAccessionNumber(issuerOfAccessionNumber); } } studyAttrs.setString(Tag.StudyInstanceUID, VR.UI, targetStudyUID); study.setAttributes(studyAttrs, archiveDeviceExtension.getAttributeFilter(Entity.Study), archiveDeviceExtension.getFuzzyStr(), archiveDeviceExtension.getNullValueForQueryFields()); em.persist(study); LOG.info("{}: QC info[createStudy] info - Creating study {}", qcSource, study); return study; } /** * Creates a new series. * The method creates a new series while copying all data from the * old series and using it to initialize the newly created series. * the method also updates the new series with the provided attributes * if the attributes are not empty. * * @param series * the series * @param target * the target * @param targetSeriesAttrs * the target series attrs * @return the series */ private Series createSeries(Series series, Study target, Attributes targetSeriesAttrs) { Attributes data = new Attributes(series.getAttributes()); data.setString(Tag.SeriesInstanceUID, VR.UI, UIDUtils.createUID()); if (targetSeriesAttrs != null) data.update(targetSeriesAttrs, data); Series newSeries = new Series(); newSeries.setStudy(target); newSeries.setSourceAET(series.getSourceAET()); newSeries.setInstitutionCode(series.getInstitutionCode()); if(data.contains(Tag.RequestAttributesSequence)) { Collection<RequestAttributes> reqAttrs = copyReqAttrs(data.getSequence(Tag.RequestAttributesSequence), newSeries); newSeries.setRequestAttributes(reqAttrs); } newSeries.setAttributes(data, archiveDeviceExtension.getAttributeFilter(Entity.Series), archiveDeviceExtension.getFuzzyStr(), archiveDeviceExtension.getNullValueForQueryFields()); em.persist(newSeries); LOG.info("{}: QC info[createSeries] info - Creating series {}", qcSource, newSeries); return newSeries; } /** * Creates a new instance. * The method creates a new instance while copying the data from * the old instance to be used for initializing the new instance. * the method also sets the instance to the series provided. * * @param oldinstance * the oldinstance * @param series * the series * @return the instance * @throws DicomServiceException * the dicom service exception */ private Instance createInstance(Instance oldinstance, Series series) throws DicomServiceException { //update with provided attrs (only attributes in the filter) Attributes data = new Attributes(oldinstance.getAttributes()); data.setString(Tag.SOPInstanceUID, VR.UI, UIDUtils.createUID()); Instance inst = new Instance(); inst.setSeries(series); inst.setConceptNameCode(oldinstance.getConceptNameCode()); if(data.contains(Tag.ContentSequence)) { Collection<ContentItem> newCItems = copyContentItems( oldinstance.getContentItems(), inst); inst.setContentItems(newCItems); } if(data.contains(Tag.VerifyingObserverSequence)) { Collection<VerifyingObserver> newVObservers = copyVerifyingObservers( data.getSequence(Tag.VerifyingObserverSequence), inst); inst.setVerifyingObservers(newVObservers); } inst.setRetrieveAETs(filterArchiveOnlyAETs(oldinstance.getAllRetrieveAETs())); inst.setAvailability(oldinstance.getAvailability()); inst.setAttributes(data, archiveDeviceExtension.getAttributeFilter(Entity.Instance), archiveDeviceExtension.getFuzzyStr(), archiveDeviceExtension.getNullValueForQueryFields()); em.persist(inst); LOG.info("{}: QC info[createInstance] info - Creating instance {}", qcSource, inst); return inst; } /** * Copy requested attributes. * The method copies the requested attributes from one series * to be used by the {@link #createSeries(Series, Study, Attributes)} method. * * @param requestedAttrsSeq * the requested attrs seq * @param newSeries * the new series * @return the collection */ private Collection<RequestAttributes> copyReqAttrs( Sequence requestedAttrsSeq, Series newSeries) { //avoid null pointer on series with no request attributes if(requestedAttrsSeq == null) return new ArrayList<RequestAttributes>(); Collection<RequestAttributes> reqAttrs = new ArrayList<RequestAttributes>(); for(Attributes attrs : requestedAttrsSeq) { RequestAttributes newReqAttr = new RequestAttributes( attrs,attrs.contains(Tag.IssuerOfAccessionNumberSequence)?findIssuerOfAccessionNumber(attrs):null, archiveDeviceExtension.getFuzzyStr(), archiveDeviceExtension.getNullValueForQueryFields()); newReqAttr.setSeries(newSeries); reqAttrs.add(newReqAttr); } return reqAttrs; } /** * Copy verifying observers. * Used by the {@link #createInstance(Instance, Series)} * to copy verifying observers associated with the old instance. * * @param oldVerifyingObserverSeq * the old verifying observer seq * @param inst * the inst * @return the collection */ private Collection<VerifyingObserver> copyVerifyingObservers( Sequence oldVerifyingObserverSeq, Instance inst) { Collection<VerifyingObserver> verifyingObservers = new ArrayList<VerifyingObserver>(); for(Attributes observer : oldVerifyingObserverSeq) { VerifyingObserver newObserver = new VerifyingObserver( observer,archiveDeviceExtension.getFuzzyStr(), archiveDeviceExtension.getNullValueForQueryFields()); newObserver.setInstance(inst); verifyingObservers.add(newObserver); } return verifyingObservers; } /** * Copy content items. * Used by the {@link #createInstance(Instance, Series)} method * to copy the content sequence from one instance to another. * * @param contentItems * the content items * @param newInstance * the new instance * @return the collection */ private Collection<ContentItem> copyContentItems( Collection<ContentItem> contentItems, Instance newInstance) { Collection<ContentItem> newContentItems = new ArrayList<ContentItem>(); for(ContentItem item: contentItems) { ContentItem newItem = new ContentItem(); newItem.setConceptCode(item.getConceptCode()); newItem.setConceptName(item.getConceptName()); newItem.setRelationshipType(item.getRelationshipType()); newItem.setTextValue(item.getTextValue()); newItem.setInstance(newInstance); newContentItems.add(newItem); } return newContentItems; } /** * Adds the identical document sequence. * Adds an instance reference in the identical document sequence * of a KO/SR object. * The method will create an identical document sequence if not found. * * @param targetIdent * the KO/SR to be updated * @param newInstance * the new instance to add reference for in the KO/SR */ private void addIdenticalDocumentSequence(Instance targetIdent, Instance newInstance) { Attributes identicalDocumentAttributes = targetIdent.getAttributes(); Attributes newStudyItem = new Attributes(); newStudyItem.setString(Tag.StudyInstanceUID, VR.UI, newInstance.getSeries().getStudy().getStudyInstanceUID()); Sequence SeriesSequence = newStudyItem.newSequence(Tag.ReferencedSeriesSequence, 1); Attributes newSeriesItem = new Attributes(); newSeriesItem.setString(Tag.SeriesInstanceUID, VR.UI, newInstance.getSeries().getSeriesInstanceUID()); Sequence SopSequence = newSeriesItem.newSequence(Tag.ReferencedSOPSequence, 1); Attributes newSopAttributes = new Attributes(); newSopAttributes.setString(Tag.ReferencedSOPInstanceUID, VR.UI, newInstance.getSopInstanceUID()); newSopAttributes.setString(Tag.ReferencedSOPClassUID, VR.UI, newInstance.getSopClassUID()); SopSequence.add(newSopAttributes); SeriesSequence.add(newSeriesItem); if (identicalDocumentAttributes.contains(Tag.IdenticalDocumentsSequence)) { Sequence seq = identicalDocumentAttributes.getSequence( Tag.IdenticalDocumentsSequence); if(!seq.contains(newStudyItem)) seq.add(newStudyItem); } else { Sequence seq = identicalDocumentAttributes.newSequence( Tag.IdenticalDocumentsSequence, 1); if(!seq.contains(newStudyItem)) seq.add(newStudyItem); } LOG.info("{} : QC info[addIdenticalDocumentSequence] info - " + "Added Identical Document Sequence to reference {} " + " to instance {}",qcSource, newInstance, targetIdent); targetIdent.getAttributesBlob().setAttributes(identicalDocumentAttributes); } /** * Removes the identical document sequence. * removes a reference from the identical document sequence of a * provided KO/SR. * The method will do nothing if no identical document sequence found. * * @param removeFromIdent * the KO/SR to be updated * @param oldInstance * the old instance to remove reference for in the KO/SR */ private void removeIdenticalDocumentSequence(Instance removeFromIdent, Instance oldInstance) { Attributes identicalDocumentAttributes = removeFromIdent.getAttributes(); for (Attributes identStudyItems : identicalDocumentAttributes .getSequence(Tag.IdenticalDocumentsSequence)) { for ( Iterator<Attributes> iter = identStudyItems .getSequence(Tag.ReferencedSeriesSequence).iterator();iter.hasNext();) { Attributes identSeriesItems = iter.next(); for ( Iterator<Attributes> sopIter = identSeriesItems .getSequence(Tag.ReferencedSOPSequence).iterator(); sopIter.hasNext();) { Attributes identSopItems = sopIter.next(); if (identSopItems.getString(Tag.ReferencedSOPInstanceUID) .equals(oldInstance.getSopInstanceUID())) { iter.remove(); } } } } LOG.info("{} : QC info[removeIdenticalDocumentSequence] info - " + "Removed Identical Document Sequence item referencing {} " + " from instance {}",qcSource, oldInstance, removeFromIdent); removeFromIdent.setAttributes(identicalDocumentAttributes, archiveDeviceExtension .getAttributeFilter(Entity.Instance), archiveDeviceExtension.getFuzzyStr(), archiveDeviceExtension.getNullValueForQueryFields()); } /** * Gets the identical document referenced instances. * Retrieves a list of all instances referenced * in the identical document sequence within the instance * @param inst * the instance containing the identical document sequence * @return the identical document referenced instances collection */ private Collection<Instance> findIdenticalDocumentReferences(Instance inst) { Collection<Instance> foundIdents = new ArrayList<Instance>(); Attributes attrs = inst.getAttributes(); if(attrs.contains(Tag.IdenticalDocumentsSequence)) { for(Attributes studyItems : attrs.getSequence(Tag.IdenticalDocumentsSequence)) { for(Attributes seriesItems : studyItems.getSequence(Tag.ReferencedSeriesSequence)) { for(Attributes sopItems : seriesItems.getSequence(Tag.ReferencedSOPSequence)) { Instance newInstance = findInstance(sopItems.getString(Tag.ReferencedSOPInstanceUID)); foundIdents.add(newInstance); } } } } return foundIdents; } /** * Gets the reference state. * Returns a state depending on the provided instances and the references * within the current requested procedure evidence sequence. * If the instances were all moved (they are all referenced) * {@link ReferenceState#ALLMOVED} is returned. * If some of the instances were moved (not all moved were referenced or * more instances were referenced than those moved) * {@link ReferenceState#SOMEMOVED} is returned. * If none of the provided instances were referenced * {@link ReferenceState#NONEMOVED} is returned. * * @param studyInstanceUID * the study instance uid * @param series * the series * @param sourceUIDs * the moved instances * @return the reference state */ private ReferenceState findReferenceState(String studyInstanceUID, Instance inst, Collection<InstanceIdentifier> sourceUIDs, String seriesType) { Sequence currentEvidenceSequence = inst.getAttributes() .getSequence(Tag.CurrentRequestedProcedureEvidenceSequence); Sequence referencedSeriesSeq = inst.getAttributes() .getSequence(Tag.ReferencedSeriesSequence); Collection<String> allReferencedSopUIDs = seriesType.equals("PR")? findAggregatedReferencedSopInstancesForSeries(referencedSeriesSeq): findAggregatedReferencedSopInstancesForStudy( studyInstanceUID, currentEvidenceSequence); int allReferencesCount=allReferencedSopUIDs.size(); if(allReferencesCount==0) return ReferenceState.NONEMOVED; for(InstanceIdentifier moved : sourceUIDs) { if(allReferencedSopUIDs.contains(moved.getSopInstanceUID())) allReferencesCount--; } if(allReferencesCount == 0) { return ReferenceState.ALLMOVED; } else if(allReferencesCount < allReferencedSopUIDs.size() && allReferencesCount > 0){ return ReferenceState.SOMEMOVED; } return ReferenceState.NONEMOVED; } /** * Gets the aggregated referenced SOP instances for study. * Helper method used to get all the referenced instances found * in the current requested procedure evidence sequence. * * @param studyInstanceUID * the study instance UID * @param currentEvidenceSequence * the current evidence sequence * @return the aggregated referenced SOP instances for study */ private Collection<String> findAggregatedReferencedSopInstancesForStudy( String studyInstanceUID, Sequence currentEvidenceSequence) { Collection<String> aggregatedSopUIDs = new ArrayList<String>(); for(Attributes studyItems : currentEvidenceSequence) { if(studyItems.getString(Tag.StudyInstanceUID).equals(studyInstanceUID)) { for(Attributes seriesItems : studyItems.getSequence(Tag.ReferencedSeriesSequence)) { for(Attributes sopItems : seriesItems.getSequence(Tag.ReferencedSOPSequence)) { aggregatedSopUIDs.add(sopItems.getString(Tag.ReferencedSOPInstanceUID)); } } } } return aggregatedSopUIDs; } private Collection<String> findAggregatedReferencedSopInstancesForSeries( Sequence referencedSeriesSequence) { Collection<String> aggregatedSopUIDs = new ArrayList<String>(); for(Attributes seriesItems : referencedSeriesSequence) { for(Attributes sopItems : seriesItems.getSequence(Tag.ReferencedImageSequence)) { aggregatedSopUIDs.add(sopItems.getString(Tag.ReferencedSOPInstanceUID)); } } return aggregatedSopUIDs; } /** * Find series KO/PR/SR. * Returns all series found within the study with the modality * types of KO (key objects), SR (structured reports) and * PS (Presentation state). * * @param parentStudy * the parent study * @return the collection */ private Collection<Series> findSeriesKOPRSR(Study parentStudy) { Collection<Series> seriesColl = new ArrayList<Series>(); for(Series series: parentStudy.getSeries()) { if(series.getModality().equals("KO") || series.getModality().equals("SR") || series.getModality().equals("PR")) { seriesColl.add(series); } } return seriesColl; } /** * Handle KO/PR/SR. * Uses the referenced state retrieved from * {@link #getReferenceState(String, Series, Collection)} * to decide whither a clone or a move is to be applied on * the found invisible objects. * The method also takes care of updating the identical document sequence * for third party objects as well as for the cloned objects. * @param targetSeriesAttrs * the target series attrs * @param qcRejectionCode * the qc rejection code * @param studyHistory * the study history * @param sourceUIDs * the source ui ds * @param targetUIDs * the target ui ds * @param sourceStudy * the source study * @param targetStudy * the target study * @param oldToNewSeries * the old to new series * @return the collection */ private Collection<InstanceHistory> handleKOPRSR(org.dcm4che3.data.Code qcRejectionCode, StudyHistory studyHistory, Collection<InstanceIdentifier> sourceUIDs, Collection<InstanceIdentifier> targetUIDs, Study sourceStudy, Study targetStudy, HashMap<String, NewSeriesTuple> oldToNewSeries) { sourceStudy = em.find(Study.class, sourceStudy.getPk()); Collection<InstanceHistory> instancesHistory = new ArrayList<InstanceHistory>(); Series newSeries; Collection<Series> seriesKOPRSR = findSeriesKOPRSR(sourceStudy); for(Series series: seriesKOPRSR) { for(Instance inst:series.getInstances()) { ReferenceState referenceState = null; if(series.getModality().equals("PR")) { referenceState = findReferenceState( sourceStudy.getStudyInstanceUID(),inst,sourceUIDs, "PR"); } else { referenceState = findReferenceState( sourceStudy.getStudyInstanceUID(),inst,sourceUIDs, "KOSR"); } Instance newInstance ; InstanceHistory instanceHistory; switch (referenceState) { case ALLMOVED: LOG.info("{} : QC info[handleKOPRSR] info - " + " All referenced items within {} moved " + " attempting to move {}", qcSource, series.getModality(), series.getModality()); if(!oldToNewSeries.keySet().contains( inst.getSeries().getSeriesInstanceUID())) { newSeries= createSeries(inst.getSeries(), targetStudy, null); SeriesHistory seriesHistory = createQCSeriesHistory(series.getSeriesInstanceUID(), series.getAttributes(), studyHistory, null); oldToNewSeries.put(inst.getSeries().getSeriesInstanceUID(), new NewSeriesTuple(newSeries.getPk(),seriesHistory)); } else { newSeries = em.find(Series.class,oldToNewSeries.get( inst.getSeries().getSeriesInstanceUID()).getPK()); } newInstance= move(inst,newSeries,qcRejectionCode); instanceHistory = new InstanceHistory( targetStudy.getStudyInstanceUID(), newSeries.getSeriesInstanceUID(), inst.getSopInstanceUID(), newInstance.getSopInstanceUID(), newInstance.getSopInstanceUID(), false); instanceHistory.setSeries(oldToNewSeries.get(inst.getSeries() .getSeriesInstanceUID()).getSeriesHistory()); instancesHistory.add(instanceHistory); targetUIDs.add(new InstanceIdentifierImpl(targetStudy.getStudyInstanceUID(), newSeries.getSeriesInstanceUID(), newInstance.getSopInstanceUID())); sourceUIDs.add(new InstanceIdentifierImpl(sourceStudy.getStudyInstanceUID(), series.getSeriesInstanceUID(), inst.getSopInstanceUID())); if(series.getModality().equals("KO") || series.getModality().equals("SR")) { for(Instance ident: findIdenticalDocumentReferences(newInstance)) { removeIdenticalDocumentSequence(ident, inst); removeIdenticalDocumentSequence(inst,ident); addIdenticalDocumentSequence(ident, newInstance); //addIdenticalDocumentSequence(newInstance, ident); already there if(!identicalDocumentSequenceHistoryExists(ident, studyHistory.getAction()) && !ident.getSeries().getStudy().getStudyInstanceUID() .equals(sourceStudy.getStudyInstanceUID())) addIdenticalDocumentSequenceHistory(ident, studyHistory.getAction()); } } break; case SOMEMOVED: LOG.info("{} : QC info[handleKOPRSR] info - " + " Some referenced items within {} moved " + " attempting to create a copy {}", qcSource, series.getModality(), series.getModality()); if(!oldToNewSeries.keySet().contains( inst.getSeries().getSeriesInstanceUID())) { newSeries= createSeries(inst.getSeries(), targetStudy, null); SeriesHistory seriesHistory = createQCSeriesHistory(series.getSeriesInstanceUID(), series.getAttributes(), studyHistory, null); oldToNewSeries.put(inst.getSeries().getSeriesInstanceUID(), new NewSeriesTuple(newSeries.getPk(),seriesHistory)); } else { newSeries = em.find(Series.class,oldToNewSeries.get( inst.getSeries().getSeriesInstanceUID()).getPK()); } newInstance = clone(inst,newSeries); inst = em.find(Instance.class, inst.getPk()); newInstance = em.find(Instance.class, newInstance.getPk()); instanceHistory = new InstanceHistory( targetStudy.getStudyInstanceUID(), newSeries.getSeriesInstanceUID(), inst.getSopInstanceUID(), newInstance.getSopInstanceUID(), newInstance.getSopInstanceUID(), true); instanceHistory.setSeries(oldToNewSeries.get(inst.getSeries() .getSeriesInstanceUID()).getSeriesHistory()); instancesHistory.add(instanceHistory); targetUIDs.add(new InstanceIdentifierImpl(targetStudy.getStudyInstanceUID(), newSeries.getSeriesInstanceUID(), newInstance.getSopInstanceUID())); sourceUIDs.add(new InstanceIdentifierImpl(sourceStudy.getStudyInstanceUID(), series.getSeriesInstanceUID(), inst.getSopInstanceUID())); if(series.getModality().equals("KO") || series.getModality().equals("SR")) { addIdenticalDocumentSequence(newInstance, inst); addIdenticalDocumentSequence(inst, newInstance); for(Instance ident: findIdenticalDocumentReferences(newInstance)) { if(ident.getPk()!=inst.getPk()) { addIdenticalDocumentSequence(ident, newInstance); // addIdenticalDocumentSequence(newInstance, ident); already exists if(!identicalDocumentSequenceHistoryExists(ident, studyHistory.getAction()) && !ident.getSeries().getStudy().getStudyInstanceUID() .equals(sourceStudy.getStudyInstanceUID())) addIdenticalDocumentSequenceHistory(ident, studyHistory.getAction()); } } } break; case NONEMOVED: LOG.info("{} : QC info[handleKOPRSR] info - " + " No referenced items within {} where " + "moved during the operation, no invisible object handling required", qcSource, series.getModality()); break; default: break; } } } return instancesHistory; } /** * Generate QC action. * Creates a new QCAction History and persists it. * * @param operation * the operation * @return the QC action history */ @Override public ActionHistory generateQCAction(QC_OPERATION operation, ActionHistory.HierarchyLevel hierarchyLevel) { ActionHistory action = new ActionHistory(); action.setCreatedTime(new Date()); action.setAction(operation.toString()); action.setHierarchyLevel(hierarchyLevel); em.persist(action); return action; } /** * Record history entry. * Persists the provided history instances. * Takes care to call {@link #updateOldHistoryRecords(Collection)} * * @param instances * the instances to be persisted */ private void recordHistoryEntry(Collection<InstanceHistory> instances) { updateOldHistoryRecords(instances); for(InstanceHistory instance : instances) { LOG.debug("{} : QC info[recordHistoryEntry] info - " + "Adding history entry {}",qcSource,instance.toString()); em.persist(instance); } } /** * Update old history records. * Updates the current for the instance, series and study * for the old instance history records changed by the qc operation. * * @param instanceRecords * the instance records to be updated */ private void updateOldHistoryRecords(Collection<InstanceHistory> instanceRecords) { Collection<InstanceHistory> associatedRecords = new ArrayList<InstanceHistory>(); for(InstanceHistory newInstanceRecord : instanceRecords) { if(!newInstanceRecord.isCloned()) { if(!newInstanceRecord.getSeries().getStudy().getAction().getAction().equalsIgnoreCase(QC_OPERATION.REJECT.name())) { associatedRecords = findInstanceHistoryByCurrentUID(newInstanceRecord.getOldUID()); for(InstanceHistory associatedRecord : associatedRecords) { if(associatedRecord.getNextUID().equals(associatedRecord.getCurrentUID())) { associatedRecord.setNextUID(newInstanceRecord.getOldUID()); } associatedRecord.setCurrentUID(newInstanceRecord.getCurrentUID()); associatedRecord.setCurrentSeriesUID(newInstanceRecord.getCurrentSeriesUID()); associatedRecord.setCurrentStudyUID(newInstanceRecord.getCurrentStudyUID()); } } } } if(associatedRecords.isEmpty()) LOG.debug("{} : QC info[updateOldHistoryRecords] info - No associated history" + " records found, no history update required", qcSource); for(InstanceHistory record : associatedRecords) { LOG.info("{} : QC info[updateOldHistoryRecords] info - Updating the following QCHistory Records" + " for referential integrity :\n", qcSource); LOG.info(record.toString()); } } /** * Gets the records by current UID. * Retrieves an instance history record by the current UID. * * @param oldUID * the old UID * @return the records found by current UID */ @SuppressWarnings("unchecked") private Collection<InstanceHistory> findInstanceHistoryByCurrentUID(String oldUID) { Query query = em.createNamedQuery(InstanceHistory.FIND_BY_CURRENT_UID); query.setParameter(1, oldUID); return query.getResultList(); } /** * Record update history entry. * Creates an update History entry and persists it. * The method takes care to set the attributes of the * history entry to the old attributes to allow for undo. * * @param action * the action * @param scope * the scope * @param unmodified * the unmodified */ private void addUpdateHistoryEntry(ActionHistory action, UpdateHistory.UpdateScope scope, Attributes unmodified, String patientPK) { UpdateHistory updateHistory = new UpdateHistory(); updateHistory.setCreatedTime(new Date()); updateHistory.setScope(scope); updateHistory.setUpdatedAttributesBlob(new AttributesBlob(unmodified)); switch (scope) { case PATIENT: updateHistory.setObjectUID(patientPK); break; case STUDY: updateHistory.setObjectUID(unmodified.getString(Tag.StudyInstanceUID)); break; case SERIES: updateHistory.setObjectUID(unmodified.getString(Tag.SeriesInstanceUID)); break; case INSTANCE: updateHistory.setObjectUID(unmodified.getString(Tag.SOPInstanceUID)); break; default: LOG.error("{} : QC info[adUpdateHistoryEntry] Failure - " + "Unsupported Scode provided to update history creation", qcSource); throw new EJBException(); } UpdateHistory prevHistoryNode = findPreviousHistoryNode( updateHistory.getObjectUID()); if(prevHistoryNode != null) { prevHistoryNode.setNext(updateHistory); } em.persist(updateHistory); } /** * Filter QCed * Removes QCed instances form a collection and returns * the updated collection. * Used by the restore method to not restore QCed instances. * * @param instances * the instances * @return the collection */ private Collection<Instance> filterQCed(Collection<Instance> instances) { Collection<Instance> instancesFiltered = instances; for(Iterator<Instance> iter = instancesFiltered.iterator(); iter.hasNext();) { Instance inst = iter.next(); if(isQCed(inst.getSopInstanceUID())) iter.remove(); LOG.debug("{} : QC info[filterQCed] Failure - " + " Unable to restore {} - still QCed",qcSource, inst); } return instancesFiltered; } /** * Checks if an instance is QCed. * Helper method for {@link #filterQCed(Collection)} * * @param sopInstanceUID * the sop instance uid * @return true, if is q ced */ private boolean isQCed(String sopInstanceUID) { return getQCInstanceHistory(sopInstanceUID).size() > 0; } @SuppressWarnings("unchecked") private List<InstanceHistory> getQCInstanceHistory(String sopIUID) { Query query = em.createNamedQuery(InstanceHistory.FIND_BY_OLD_UID); query.setParameter(1, sopIUID); return (List<InstanceHistory>) query.getResultList(); } /** * Gets the previous history node. * Used to set the next pk in a chain of updates. * * @param objectUID * the object uid * @return the prev history node */ private UpdateHistory findPreviousHistoryNode(String objectUID) { Query query = em.createNamedQuery(UpdateHistory.FIND_FIRST_UPDATE_ENTRY); query.setParameter(1, objectUID); UpdateHistory result=null; if(query.getResultList().size()>0) result= (UpdateHistory) query.getSingleResult(); return result; } /** * Adds the identical document sequence history. * Adds a history entry for third party studies, series and instances * with identical documents to the QCed ones * * @param ident * the instance containing the * identical document sequence * @param identHistoryAction * the history action for the operation */ private void addIdenticalDocumentSequenceHistory(Instance ident, ActionHistory identHistoryAction) { StudyHistory identStudyHistory = createQCStudyHistory( ident.getSeries().getStudy().getStudyInstanceUID(), ident.getSeries().getStudy().getStudyInstanceUID(), new Attributes(), identHistoryAction); SeriesHistory identSeriesHistory = createQCSeriesHistory( ident.getSeries().getSeriesInstanceUID(), new Attributes(), identStudyHistory, null); InstanceHistory identInstanceHistory = new InstanceHistory( ident.getSopInstanceUID(), identSeriesHistory.getOldSeriesUID(), ident.getSopInstanceUID(), ident.getSopInstanceUID(), ident.getSopInstanceUID(), false); identInstanceHistory.setPreviousAtributesBlob(ident.getAttributesBlob()); identInstanceHistory.setSeries(identSeriesHistory); em.persist(identInstanceHistory); LOG.info("{} : QC info[addIdenticalDocumentSequence] info - " + " Added identical document sequence history entry for {}", qcSource, ident); } /** * Identical document sequence history exists. * Checks for the existence of a history entry * with identical * @param ident * the instance containing * the identical document sequence * @param identHistoryAction * the history action for the operation * @return true, if successful */ private boolean identicalDocumentSequenceHistoryExists(Instance ident, ActionHistory identHistoryAction) { InstanceHistory identHistory = findIdenticalDocumentHistory(ident, identHistoryAction); return identHistory==null ? false : true; } /** * Find a history instance for an identical document. * Returns the instance if found. * Used to check if the instance was already persisted in the same action. * * @param ident * the ident * @param identHistoryAction * the ident history action * @return the QC instance history */ private InstanceHistory findIdenticalDocumentHistory(Instance ident, ActionHistory identHistoryAction) { InstanceHistory result; Query query = em.createNamedQuery(InstanceHistory.FIND_BY_CURRENT_UID_FOR_ACTION); query.setParameter(1, ident.getSopInstanceUID()); query.setParameter(2, identHistoryAction.getAction()); try { result = (InstanceHistory) query.getSingleResult(); } catch (NoResultException e) { LOG.error("{} : QC info[findIdenticalDocumentHistory] Failure - " + " Unable to find identical document sequence history ", qcSource); result = null; } return result; } private ArchiveAEExtension getAEExtensionForRejectionNoteStorage() { ArchiveDeviceExtension archiveDeviceExtension = device.getDeviceExtension(ArchiveDeviceExtension.class); String defaultAE = archiveDeviceExtension.getDefaultAETitle(); ArchiveAEExtension arcAEExt = device.getApplicationEntity(defaultAE).getAEExtension(ArchiveAEExtension.class); if (arcAEExt == null) { LOG.error("Could not resolve Application Entity to use for storing Rejection-Note"); throw new EJBException("Can not store Rejection-Note after QC operation! Could not resolve Application Entity to use for storing Rejection-Note locally, device: " + device.getDeviceName()); } return arcAEExt; } private Instance createAndStoreRejectionNote(org.dcm4che3.data.Code rejectionCode, Collection<Instance> instances) { if (instances != null && instances.size() > 0) { Attributes rejNote = createRejectionNote(rejectionCode, instances); ArchiveAEExtension arcAEExt = getAEExtensionForRejectionNoteStorage(); try { List<Connection> conns = arcAEExt.getApplicationEntity().getConnections(); String hostname = conns.isEmpty() ? "UNKNOWN" : conns.get(0).getHostname(); StoreSession session = storeService.createStoreSession(storeService); session.setSource(new GenericParticipant(hostname, "QCAction")); session.setRemoteAET(arcAEExt.getApplicationEntity().getAETitle()); session.setArchiveAEExtension(arcAEExt); storeService.init(session); StoreContext context = storeService.createStoreContext(session); Attributes fmi = new Attributes(); fmi.setString(Tag.TransferSyntaxUID, VR.UI, UID.ImplicitVRLittleEndian); storeService.writeSpoolFile(context, fmi, rejNote); storeService.store(context); LOG.debug("RejectionNote stored! instance:{}", context.getInstance()); return context.getInstance(); } catch (DicomServiceException x) { LOG.error("Failed to store RejectionNote!", x); throw new EJBException(x); } } return null; } private Attributes createRejectionNote(org.dcm4che3.data.Code rejectionCode, Collection<Instance> instances) { Attributes kos = createKOS(rejectionCode, instances.iterator().next()); Sequence evidenceSeq = kos.newSequence(Tag.CurrentRequestedProcedureEvidenceSequence, 1); Sequence contentSeq = kos.newSequence(Tag.ContentSequence, 1); HashMap<Long, Attributes> mapEvidenceItem = new HashMap<Long, Attributes>(); HashMap<Long, Attributes> mapRefSeriesItem = new HashMap<Long, Attributes>(); Attributes evidenceItem, refSeriesItem, contentItem; Sequence refSeriesSeq; for (Instance inst : instances) { evidenceItem = mapEvidenceItem.get(inst.getSeries().getStudy().getPk()); if (evidenceItem == null) { evidenceItem = new Attributes(); evidenceItem.setString(Tag.StudyInstanceUID, VR.UI, inst.getSeries().getStudy().getStudyInstanceUID()); evidenceItem.newSequence(Tag.ReferencedSeriesSequence, 1); evidenceSeq.add(evidenceItem); mapEvidenceItem.put(inst.getSeries().getStudy().getPk(), evidenceItem); } refSeriesSeq = evidenceItem.getSequence(Tag.ReferencedSeriesSequence); refSeriesItem = mapRefSeriesItem.get(inst.getSeries().getPk()); if (refSeriesItem == null) { refSeriesItem = new Attributes(); refSeriesItem.setString(Tag.SeriesInstanceUID, VR.UI, inst.getSeries().getSeriesInstanceUID()); refSeriesItem.newSequence(Tag.ReferencedSOPSequence, 1); refSeriesSeq.add(refSeriesItem); mapRefSeriesItem.put(inst.getSeries().getPk(), refSeriesItem); } addReferencedSopSeqItem(refSeriesItem, inst); contentItem = new Attributes(); contentItem.setString(Tag.ValueType, VR.CS, getValueType(inst.getSopClassUID())); contentItem.setString(Tag.RelationshipType, VR.CS, "CONTAINS"); contentItem.newSequence(Tag.ReferencedSOPSequence, 1); addReferencedSopSeqItem(contentItem, inst); contentSeq.add(contentItem); } return kos; } private void addReferencedSopSeqItem(Attributes attrs, Instance inst) { Attributes refSopItem = new Attributes(); refSopItem.setString(Tag.ReferencedSOPInstanceUID, VR.UI, inst.getSopInstanceUID()); refSopItem.setString(Tag.ReferencedSOPClassUID, VR.UI, inst.getSopClassUID()); attrs.getSequence(Tag.ReferencedSOPSequence).add(refSopItem); } private Attributes createKOS(org.dcm4che3.data.Code rejectionCode, Instance instance) { Attributes attrs = instance.getSeries().getStudy().getPatient().getAttributes(); attrs.addAll(instance.getSeries().getStudy().getAttributes()); Attributes kos = new Attributes(attrs, PATIENT_AND_STUDY_ATTRS); kos.setString(Tag.SOPClassUID, VR.UI, UID.KeyObjectSelectionDocumentStorage); kos.setString(Tag.SOPInstanceUID, VR.UI, UIDUtils.createUID()); kos.setDate(Tag.ContentDateAndTime, new Date()); kos.setString(Tag.Modality, VR.CS, "KO"); kos.setNull(Tag.ReferencedPerformedProcedureStepSequence, VR.SQ); kos.setString(Tag.SeriesInstanceUID, VR.UI, getRejectionNoteSeriesUID(kos.getString(Tag.StudyInstanceUID))); kos.setString(Tag.SeriesNumber, VR.IS, "999"); kos.setString(Tag.SeriesDescription, VR.LO, "Rejection Note"); kos.setString(Tag.InstanceNumber, VR.IS, "1"); kos.setString(Tag.ValueType, VR.CS, "CONTAINER"); kos.setString(Tag.ContinuityOfContent, VR.CS, "SEPARATE"); kos.newSequence(Tag.ConceptNameCodeSequence, 1).add(rejectionCode.toItem()); Attributes tmplItem = new Attributes(2); tmplItem.setString(Tag.MappingResource, VR.CS, "DCMR"); tmplItem.setString(Tag.TemplateIdentifier, VR.CS, "2010"); kos.newSequence(Tag.ContentTemplateSequence, 1).add(tmplItem); return kos; } private String getRejectionNoteSeriesUID(String studyIUID) { Query q = em.createQuery("SELECT s.seriesInstanceUID from Series s WHERE s.study.studyInstanceUID = ?1 AND s.seriesDescription = ?2"); q.setParameter(1, studyIUID); q.setParameter(2, "Rejection Note"); @SuppressWarnings("unchecked") List<String> uids = q.getResultList(); return uids.isEmpty() ? UIDUtils.createUID() : uids.get(0); } private static String getValueType(String sopClassUID) { RecordType rt = recordFactory.getRecordType(sopClassUID); return (rt == RecordType.IMAGE || rt == RecordType.WAVEFORM) ? rt.name() : "COMPOSITE"; } /** * A tuple that carries a series instance UID for a new series as well as a * SeriesHistory entry Used to associate one series only to one history * entry to be passed to each instance history created of the same series. * * @author Hesham Elbadawi <bsdreko@gmail.com> */ private static class NewSeriesTuple { private final long pk; private final SeriesHistory seriesHistory; /** * Instantiates a new new series tuple. * * @param pk * the pk * @param seriesHistory * the series history */ NewSeriesTuple(long pk, SeriesHistory seriesHistory) { this.pk = pk; this.seriesHistory = seriesHistory; } /** * Gets the pk. * * @return the pk */ public long getPK() { return pk; } /** * Gets the series history. * * @return the series history */ public SeriesHistory getSeriesHistory() { return seriesHistory; } } private static class PatientAttrsPKTuple { private final long pk; private final Attributes unmodified; private PatientAttrsPKTuple(long pk, Attributes attrs) { this.pk= pk; this.unmodified = attrs; } public long getPK() { return this.pk; } public Attributes getUnModifiedAttrs() { return this.unmodified; } } private boolean isQCPermittedForStudy(Study study) { for(StudyProtectionHook studyProtectionHook : studyProtectionHooks) { if(studyProtectionHook.isProtected(study.getStudyInstanceUID())) { return false; } } return true; } private void checkIfQCPermittedForStudy(Study study) throws QCOperationNotPermittedException { if(!isQCPermittedForStudy(study)) { throw new QCOperationNotPermittedException("QC operation is not allowed for protected study with UID " + study.getStudyInstanceUID()); } } private String[] filterArchiveOnlyAETs(String[] allRetrieveAETs) { List<String> filteredLocalOnly = new ArrayList<String>(); for(String aet : allRetrieveAETs) { if(device.getApplicationAETitles().contains(aet)); filteredLocalOnly.add(aet); } return filteredLocalOnly.toArray(new String[] {}); } /* * Schedules a change request that will be fired after the SC transaction has committed successfully * -> Do not send change request to external system if SC transaction failed! */ private void scheduleChangeRequestAfterTxCommit(final QCOperationContext qcCtx) { final ChangeRequestContext changeRequestCtx = changeRequester.createChangeRequestContext(qcCtx.getSourceInstances(), qcCtx.getTargetInstances(), qcCtx.getRejectionNotes()); Executor executor = new Executor() { @Override public void execute(Runnable r) { platformExecutor.asyncExecute(r); } }; transactionSynchronization.afterSuccessfulCommit(new Runnable() { @Override public void run() { changeRequester.scheduleChangeRequest(changeRequestCtx); } }, executor); } }