Java tutorial
/* ***** 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/dcm4che. * * The Initial Developer of the Original Code is * Agfa Healthcare. * Portions created by the Initial Developer are Copyright (C) 2011 * 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.proxy.forward; import java.io.File; import java.io.FileFilter; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.nio.file.CopyOption; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Collection; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.Map.Entry; import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.io.FileUtils; import org.dcm4che3.conf.api.ApplicationEntityCache; import org.dcm4che3.conf.api.AttributeCoercion; import org.dcm4che3.conf.api.ConfigurationException; import org.dcm4che3.data.Attributes; import org.dcm4che3.data.Sequence; import org.dcm4che3.data.Tag; import org.dcm4che3.data.UID; import org.dcm4che3.data.VR; import org.dcm4che3.emf.MultiframeExtractor; import org.dcm4che3.io.DicomEncodingOptions; import org.dcm4che3.io.DicomInputStream; import org.dcm4che3.io.DicomInputStream.IncludeBulkData; import org.dcm4che3.io.DicomOutputStream; import org.dcm4che3.net.ApplicationEntity; import org.dcm4che3.net.Association; import org.dcm4che3.net.AssociationStateException; import org.dcm4che3.net.DataWriterAdapter; import org.dcm4che3.net.Dimse; import org.dcm4che3.net.DimseRSPHandler; import org.dcm4che3.net.IncompatibleConnectionException; import org.dcm4che3.net.NoPresentationContextException; import org.dcm4che3.net.Status; import org.dcm4che3.net.TransferCapability.Role; import org.dcm4che3.net.pdu.AAbort; import org.dcm4che3.net.pdu.AAssociateRJ; import org.dcm4che3.net.pdu.AAssociateRQ; import org.dcm4che3.net.pdu.PresentationContext; import org.dcm4che3.net.pdu.RoleSelection; import org.dcm4che3.net.service.DicomServiceException; import org.dcm4chee.proxy.Proxy; import org.dcm4chee.proxy.common.AuditDirectory; import org.dcm4chee.proxy.common.RetryObject; import org.dcm4chee.proxy.conf.ForwardOption; import org.dcm4chee.proxy.conf.ProxyAEExtension; import org.dcm4chee.proxy.conf.ProxyDeviceExtension; import org.dcm4chee.proxy.conf.Retry; import org.dcm4chee.proxy.dimse.StgCmt; import org.dcm4chee.proxy.utils.AttributeCoercionUtils; import org.dcm4chee.proxy.utils.ForwardConnectionUtils; import org.dcm4chee.proxy.utils.InfoFileUtils; import org.dcm4chee.proxy.utils.LogUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Michael Backhaus <michael.backaus@agfa.com> */ public class ForwardFiles { private final Lock lock = new ReentrantLock(); protected static final Logger LOG = LoggerFactory.getLogger(ForwardFiles.class); private ApplicationEntityCache aeCache; public ForwardFiles(ApplicationEntityCache aeCache) { this.aeCache = aeCache; } public void execute(ApplicationEntity ae) { final ProxyAEExtension proxyAEE = ae.getAEExtension(ProxyAEExtension.class); final HashMap<String, ForwardOption> forwardOptions = proxyAEE.getForwardOptions(); try { processCStore(proxyAEE, forwardOptions); processNAction(proxyAEE, forwardOptions); processNEventReport(proxyAEE, forwardOptions); ((ProxyDeviceExtension) proxyAEE.getApplicationEntity().getDevice() .getDeviceExtension(ProxyDeviceExtension.class)).getFileForwardingExecutor() .execute(new Runnable() { @Override public void run() { try { processNCreate(proxyAEE, forwardOptions); } catch (IOException e) { LOG.error("Error processing scheduled N-CREATE files: {}", e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } try { processNSet(proxyAEE, forwardOptions); } catch (IOException e) { LOG.error("Error processing scheduled N-SET files: {}", e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } }); } catch (IOException e) { LOG.error("Error scanning spool directory: {}", e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } private void processNSet(ProxyAEExtension proxyAEE, HashMap<String, ForwardOption> forwardOptions) throws IOException { for (String calledAET : proxyAEE.getNSetDirectoryPath().list(dirFilter())) { File[] files = new File(proxyAEE.getNSetDirectoryPath(), calledAET) .listFiles(fileFilter(proxyAEE, calledAET)); if (files == null || files.length == 0) continue; LOG.debug("Processing schedule N-SET data for {} ...", calledAET); if (!forwardOptions.keySet().contains(calledAET)) { // process destinations without forward schedule LOG.debug("No forward schedule for {}, sending existing N-SET data now", calledAET); startForwardScheduledMPPS(proxyAEE, files, calledAET, "nset"); } else for (Entry<String, ForwardOption> entry : forwardOptions.entrySet()) { boolean isMatchingAET = calledAET.equals(entry.getKey()); if (isMatchingAET && entry.getValue().getSchedule().isNow(new GregorianCalendar())) { LOG.debug("Found currently active forward schedule for {}, sending N-SET data now", calledAET); startForwardScheduledMPPS(proxyAEE, files, calledAET, "nset"); } else if (isMatchingAET) { LOG.debug("Found forward schedule for {}, but is inactive (days={}, hours={})", new Object[] { calledAET, entry.getValue().getSchedule().getDays(), entry.getValue().getSchedule().getHours() }); } } } } private void processNCreate(ProxyAEExtension proxyAEE, HashMap<String, ForwardOption> forwardOptions) throws IOException { for (String calledAET : proxyAEE.getNCreateDirectoryPath().list(dirFilter())) { File[] files = new File(proxyAEE.getNCreateDirectoryPath(), calledAET) .listFiles(fileFilter(proxyAEE, calledAET)); if (files == null || files.length == 0) continue; LOG.debug("Processing schedule N-CREATE data for {} ...", calledAET); if (!forwardOptions.keySet().contains(calledAET)) { // process destinations without forward schedule LOG.debug("No forward schedule for {}, sending existing N-CREATE data now", calledAET); startForwardScheduledMPPS(proxyAEE, files, calledAET, "ncreate"); } else for (Entry<String, ForwardOption> entry : forwardOptions.entrySet()) { boolean isMatchingAET = calledAET.equals(entry.getKey()); if (isMatchingAET && entry.getValue().getSchedule().isNow(new GregorianCalendar())) { LOG.debug( "Found currently active forward schedule for {}, sending existing N-CREATE data now", calledAET); startForwardScheduledMPPS(proxyAEE, files, calledAET, "ncreate"); } else if (isMatchingAET) { LOG.debug("Found forward schedule for {}, but is inactive (days={}, hours={})", new Object[] { calledAET, entry.getValue().getSchedule().getDays(), entry.getValue().getSchedule().getHours() }); } } } } private void processNAction(ProxyAEExtension proxyAEE, HashMap<String, ForwardOption> forwardOptions) throws IOException { for (String transactionUID : proxyAEE.getNactionDirectoryPath().list(dirFilter())) { File parent = new File(proxyAEE.getNactionDirectoryPath(), transactionUID); if (parent.list().length == 0) { LOG.debug("Delete empty dir {}", parent); parent.delete(); return; } for (String calledAET : parent.list()) { File dir = new File(parent, calledAET); File[] files = dir.listFiles(fileFilter(proxyAEE, calledAET)); if (files == null || files.length == 0) continue; LOG.debug("Processing schedule N-ACTION data ..."); if (!forwardOptions.keySet().contains(calledAET)) { // process destinations without forward schedule LOG.debug("No forward schedule for {}, sending existing N-ACTION data now", calledAET); startForwardScheduledNAction(proxyAEE, calledAET, files); } else for (Entry<String, ForwardOption> entry : forwardOptions.entrySet()) { boolean isMatchingAET = calledAET.equals(entry.getKey()); if (isMatchingAET && entry.getValue().getSchedule().isNow(new GregorianCalendar())) { LOG.debug( "Found currently active forward schedule for {}, sending existing N-ACTION data now", calledAET); startForwardScheduledNAction(proxyAEE, calledAET, files); } else if (isMatchingAET) { LOG.debug("Found forward schedule for {}, but is inactive (days={}, hours={})", new Object[] { calledAET, entry.getValue().getSchedule().getDays(), entry.getValue().getSchedule().getHours() }); } } } } } private void processNEventReport(ProxyAEExtension proxyAEE, HashMap<String, ForwardOption> forwardOptions) throws IOException { for (File transactionUidDir : proxyAEE.getNeventDirectoryPath().listFiles(dirFilter())) { if (folderSize(transactionUidDir) == 0) { deleteFolder(transactionUidDir); if (!transactionUidDir.exists()) { LOG.debug("Delete empty dir {}", transactionUidDir); continue; } else { LOG.debug("Failed to delete empty dir {}", transactionUidDir); } } if (StgCmt.hasPendingNActionRQ(proxyAEE, new File(proxyAEE.getNactionDirectoryPath(), transactionUidDir.getName())) || StgCmt.hasPendingNEventReportRQ(proxyAEE, transactionUidDir)) continue; processNEventReportTransactionUID(proxyAEE, forwardOptions, transactionUidDir); } } private long folderSize(File directory) { long length = 0; for (File file : directory.listFiles()) { if (file.isFile()) length += file.length(); else length += folderSize(file); } return length; } private void processNEventReportTransactionUID(ProxyAEExtension proxyAEE, HashMap<String, ForwardOption> forwardOptions, File transactionUidDir) throws IOException { String[] aets = transactionUidDir.list(); if (aets.length > 1) { mergeNEventReportRQs(proxyAEE, transactionUidDir); aets = transactionUidDir.list(); if (aets.length > 1) { LOG.error("Error merging n-event-reports in {}", transactionUidDir); return; } } File aetDir = new File(transactionUidDir, aets[0]); File[] neventFiles = aetDir.listFiles(fileFilter(proxyAEE, aets[0])); if (neventFiles == null || neventFiles.length == 0) return; if (neventFiles.length > 1) { LOG.error("Found more than one NEventReportRQ files in {}. " + "Needs to be resolved before further processing!", aetDir); return; } LOG.debug("Processing schedule N-EVENT-REPORT data ..."); startForwardScheduledNEventReport(proxyAEE, neventFiles); } private void mergeNEventReportRQs(ProxyAEExtension proxyAEE, File transactionUidDir) throws IOException { String[] aets = transactionUidDir.list(); Attributes mergedAttrs = new Attributes(); File[] nevent = null; Attributes attrs; boolean mergeUsingANDLogic = proxyAEE.isMergeStgCmtMessagesUsingANDLogic(); for (int i = 0; i < aets.length; ++i) { String aet = aets[i]; File aetDir = new File(transactionUidDir, aet); File[] neventFiles = aetDir.listFiles(fileFilter(proxyAEE, aet)); nevent = createSendFileList(neventFiles); if (nevent.length != neventFiles.length) { LOG.error("Error renaming nevent files for further processing"); resetSendFiles(nevent); return; } DicomInputStream in = new DicomInputStream(nevent[0]); try { attrs = in.readDataset(-1, -1); } finally { in.close(); } attrs = reformatReferencedSopSequenceAttrs(attrs); if (i == 0) mergedAttrs.addAll(attrs); else { Sequence mergedSequence = mergedAttrs.getSequence(Tag.ReferencedSOPSequence); matchReferencedSopSequence(attrs, mergedAttrs, mergeUsingANDLogic, mergedSequence); matchFailedSopSequence(attrs, mergedAttrs, mergeUsingANDLogic, mergedSequence); } } if (nevent.length == 0 || mergedAttrs.isEmpty()) { LOG.error("Error reading datasets from stored N-EVENT-REPORT files"); return; } Properties prop = InfoFileUtils.getFileInfoProperties(proxyAEE, nevent[0]); File mergeDir = new File(transactionUidDir, "MERGEDNEVENT"); mergeDir.mkdir(); //here check for the dicom conformance of the nevent if (mergedAttrs.getSequence(Tag.FailedSOPSequence) != null && mergedAttrs.getSequence(Tag.FailedSOPSequence).size() == 0) mergedAttrs.remove(Tag.FailedSOPSequence); //referencessop sequence can be empty alongside failed ones or not empty so no check here storeMergedNEvent(mergedAttrs, prop, mergeDir); // cleanup obsolete aet dirs and files for (int i = 0; i < aets.length; ++i) { String aet = aets[i]; File aetDir = new File(transactionUidDir, aet); nevent = aetDir.listFiles(Proxy.sndFileFilter()); deleteFile(nevent[0]); File[] naction = aetDir.listFiles(StgCmt.nactionFileFilter()); deleteFile(naction[0]); } } private void matchFailedSopSequence(Attributes attrs, Attributes mergedAttrs, boolean mergeUsingANDLogic, Sequence mergedSequence) throws DicomServiceException { if (!attrs.contains(Tag.FailedSOPSequence)) return; Sequence newFailedSopSequence = attrs.getSequence(Tag.FailedSOPSequence); Iterator<Attributes> newFailedSeqIter = newFailedSopSequence.iterator(); Sequence mergedFailedSequence; if (!mergedAttrs.contains(Tag.FailedSOPSequence)) mergedFailedSequence = mergedAttrs.newSequence(Tag.FailedSOPSequence, newFailedSopSequence.size()); else mergedFailedSequence = mergedAttrs.getSequence(Tag.FailedSOPSequence); while (newFailedSeqIter.hasNext()) { Attributes newFailedItem = newFailedSeqIter.next(); String newFailedSopInst = newFailedItem.getString(Tag.ReferencedSOPInstanceUID); Iterator<Attributes> mergedFailedSequenceIter = mergedFailedSequence.iterator(); boolean contains = false; while (mergedFailedSequenceIter.hasNext()) { Attributes failedItem = mergedFailedSequenceIter.next(); if (failedItem.getString(Tag.ReferencedSOPInstanceUID).equals(newFailedSopInst)) { contains = true; break; } } if (!contains && mergeUsingANDLogic) { Iterator<Attributes> mergedSequenceIter = mergedSequence.iterator(); while (mergedSequenceIter.hasNext()) { Attributes mergedItem = mergedSequenceIter.next(); if (mergedItem.getString(Tag.ReferencedSOPInstanceUID).equals(newFailedSopInst)) { mergedSequence.remove(mergedItem); mergedFailedSequence.add(new Attributes(newFailedItem)); break; } } } } // if (mergedFailedSequence.size() == 0) // mergedAttrs.remove(Tag.FailedSOPSequence); //this breaks the merge completely // if (mergedSequence.size() == 0) // mergedAttrs.remove(Tag.ReferencedSOPSequence); } private void matchReferencedSopSequence(Attributes attrs, Attributes mergedAttrs, boolean mergeUsingANDLogic, Sequence mergedSequence) throws DicomServiceException { Iterator<Attributes> newSequenceIter = attrs.getSequence(Tag.ReferencedSOPSequence).iterator(); while (newSequenceIter.hasNext()) { Attributes newItem = newSequenceIter.next(); String newRefSopInst = newItem.getString(Tag.ReferencedSOPInstanceUID); boolean contains = false; Iterator<Attributes> mergedSequenceIter = mergedSequence.iterator(); while (mergedSequenceIter.hasNext()) { Attributes mergedItem = mergedSequenceIter.next(); if (mergedItem.getString(Tag.ReferencedSOPInstanceUID).equals(newRefSopInst)) { contains = true; break; } } if (!contains && !mergeUsingANDLogic) { if (!mergedAttrs.contains(Tag.FailedSOPSequence)) throw new DicomServiceException(Status.UnableToPerformSubOperations); Sequence mergedFailedSequence = mergedAttrs.getSequence(Tag.FailedSOPSequence); Iterator<Attributes> mergedFailedSequenceIter = mergedFailedSequence.iterator(); while (mergedFailedSequenceIter.hasNext()) { Attributes failedItem = mergedFailedSequenceIter.next(); if (failedItem.getString(Tag.ReferencedSOPInstanceUID).equals(newRefSopInst)) { mergedFailedSequence.remove(failedItem); mergedSequence.add(new Attributes(newItem)); break; } } //handled later after all are merged // if (mergedFailedSequence.size() == 0) // mergedAttrs.remove(Tag.FailedSOPSequence); } } //will break the merge function /* if (mergedSequence.size() == 0) mergedAttrs.remove(Tag.ReferencedSOPSequence);*/ } private Attributes reformatReferencedSopSequenceAttrs(Attributes attrs) { if (attrs.contains(Tag.RetrieveAETitle) || attrs.contains(Tag.StorageMediaFileSetID)) { final String retrieveAET = attrs.getString(Tag.RetrieveAETitle); final String storageMediaFileSetID = attrs.getString(Tag.StorageMediaFileSetID); final String storageMediaFileSetUID = attrs.getString(Tag.StorageMediaFileSetUID); Iterator<Attributes> iter = attrs.getSequence(Tag.ReferencedSOPSequence).iterator(); while (iter.hasNext()) { Attributes seqAttrs = iter.next(); if (retrieveAET != null) seqAttrs.setString(Tag.RetrieveAETitle, VR.AE, retrieveAET); if (storageMediaFileSetID != null || storageMediaFileSetUID != null) { seqAttrs.setString(Tag.StorageMediaFileSetID, VR.SH, storageMediaFileSetID); seqAttrs.setString(Tag.StorageMediaFileSetUID, VR.UI, storageMediaFileSetUID); } } attrs.remove(Tag.RetrieveAETitle); attrs.remove(Tag.StorageMediaFileSetID); attrs.remove(Tag.StorageMediaFileSetUID); } return attrs; } private void storeMergedNEvent(Attributes mergedAttrs, Properties prop, File mergeDir) throws IOException, DicomServiceException, FileNotFoundException { File file; file = File.createTempFile("dcm", ".nevent", mergeDir); DicomOutputStream stream = null; try { stream = new DicomOutputStream(file); String iuid = UID.StorageCommitmentPushModelSOPInstance; String cuid = UID.StorageCommitmentPushModelSOPClass; String tsuid = UID.ExplicitVRLittleEndian; Attributes fmi = Attributes.createFileMetaInformation(iuid, cuid, tsuid); // using use-calling-aet, which is called-aet from previous // n-action-rq (cf. org.dcm4chee.proxy.dimse.StgCmt.createRQFile) fmi.setString(Tag.SourceApplicationEntityTitle, VR.AE, prop.getProperty("use-calling-aet")); LOG.debug("Create {}", file.getPath()); stream.writeDataset(fmi, mergedAttrs); } catch (Exception e) { LOG.error("Failed to create file {}: {}", new Object[] { file, e }); if (LOG.isDebugEnabled()) e.printStackTrace(); throw new DicomServiceException(Status.OutOfResources, e.getCause()); } finally { stream.close(); } File infoFile = new File(mergeDir, file.getName().substring(0, file.getName().indexOf('.')) + ".info"); FileOutputStream infoOut = new FileOutputStream(infoFile); try { LOG.debug("Create {}", infoFile); prop.store(infoOut, null); } catch (Exception e) { LOG.error("Failed to create info-file {}: {} ", file, e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); file.delete(); throw new DicomServiceException(Status.OutOfResources, e.getCause()); } finally { infoOut.close(); } } private void processCStore(ProxyAEExtension proxyAEE, HashMap<String, ForwardOption> forwardOptions) throws IOException { for (String calledAET : proxyAEE.getCStoreDirectoryPath().list(dirFilter())) { File dir = new File(proxyAEE.getCStoreDirectoryPath(), calledAET); File[] files = dir.listFiles(fileFilter(proxyAEE, calledAET)); if (files == null || files.length == 0) continue; LOG.debug("Processing schedule C-STORE data ..."); if (!forwardOptions.keySet().contains(calledAET)) { // process destinations without forward schedule LOG.debug("No forward schedule for {}, sending existing C-STORE data now", calledAET); startForwardScheduledCStoreFiles(proxyAEE, calledAET, files); } else for (Entry<String, ForwardOption> entry : forwardOptions.entrySet()) { boolean isMatchingAET = calledAET.equals(entry.getKey()); if (isMatchingAET && entry.getValue().getSchedule().isNow(new GregorianCalendar())) { LOG.debug( "Found currently active forward schedule for {}, sending existing C-STORE data now", calledAET); startForwardScheduledCStoreFiles(proxyAEE, calledAET, files); } else if (isMatchingAET) { LOG.debug("Found forward schedule for {}, but is inactive (days={}, hours={})", new Object[] { calledAET, entry.getValue().getSchedule().getDays(), entry.getValue().getSchedule().getHours() }); } } } } private FileFilter fileFilter(final ProxyAEExtension proxyAEE, final String calledAET) { final long now = System.currentTimeMillis(); return new FileFilter() { @Override public boolean accept(File file) { String path = file.getPath(); int interval = proxyAEE.getApplicationEntity().getDevice() .getDeviceExtension(ProxyDeviceExtension.class).getSchedulerInterval(); if (path.endsWith(".dcm") || path.endsWith(".nevent")) { if (now > (file.lastModified() + interval)) return true; else return false; } if (path.endsWith(".part") || path.endsWith(".snd") || path.endsWith(".info") || path.endsWith(".naction")) return false; try { LOG.debug("Get matching retry for file " + file.getPath()); String suffix = path.substring(path.lastIndexOf('.')); Retry matchingRetry = getMatchingRetry(proxyAEE, suffix); if (matchingRetry == null) if (proxyAEE.isDeleteFailedDataWithoutRetryConfiguration()) deleteFailedFile(proxyAEE, calledAET, file, ": delete files without retry configuration is ENABLED", 0); else moveToNoRetryPath(proxyAEE, calledAET, file, ": delete files without retry configuration is DISABLED"); else if (checkNumberOfRetries(proxyAEE, matchingRetry, suffix, file, calledAET) && checkSendFileDelay(now, file, matchingRetry)) return true; } catch (IndexOutOfBoundsException e) { LOG.error("Error parsing suffix of " + path); try { moveToNoRetryPath(proxyAEE, calledAET, file, "(error parsing suffix)"); } catch (IOException e1) { LOG.error("Error moving file {} to no retry directory: {}", new Object[] { file.getName(), e.getMessage() }); if (LOG.isDebugEnabled()) e1.printStackTrace(); } } catch (IOException e) { LOG.error("Error reading from directory: {}", e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } return false; } private boolean checkSendFileDelay(final long now, File file, Retry matchingRetry) { boolean sendNow = now > (file.lastModified() + (matchingRetry.delay * 1000)); if (sendNow) LOG.debug(">> ready to send now"); else LOG.debug(">> wait until last send delay > {}sec", matchingRetry.delay); return sendNow; } }; } private FilenameFilter dirFilter() { return new FilenameFilter() { @Override public boolean accept(File dir, String name) { return new File(dir, name).isDirectory(); } }; } private boolean checkNumberOfRetries(ProxyAEExtension proxyAEE, Retry retry, String suffix, File file, String calledAET) throws IOException { LOG.debug("Check number of previous retries for file " + file.getPath()); int prevRetries = 0; String substring = suffix.substring(retry.getRetryObject().getSuffix().length()); if (!substring.isEmpty()) try { prevRetries = Integer.parseInt(substring); LOG.debug(">> previous retries = " + prevRetries); } catch (NumberFormatException e) { LOG.error("Error parsing number of retries in suffix of file " + file.getName()); moveToNoRetryPath(proxyAEE, calledAET, file, ": error parsing suffix"); return false; } boolean send = prevRetries < retry.numberOfRetries; LOG.debug(">> send file again = {} (max number of retries for {} = {})", new Object[] { send, retry.getRetryObject(), retry.numberOfRetries }); if (!send) { String reason = ">> max number of retries = " + retry.getNumberOfRetries(); if (sendToFallbackAET(proxyAEE, calledAET)) { moveToFallbackAetDir(proxyAEE, file, calledAET, reason); } else if (retry.deleteAfterFinalRetry) deleteFailedFile(proxyAEE, calledAET, file, reason + " and delete after final retry is ENABLED", prevRetries); else { moveToNoRetryPath(proxyAEE, calledAET, file, reason); } } return send; } private void moveToFallbackAetDir(ProxyAEExtension proxyAEE, File file, String calledAET, String reason) throws IOException { String path = file.getAbsolutePath(); if (path.contains("ncreate")) { File nSetFile = getMatchingNsetFile(proxyAEE, calledAET, file); if (nSetFile != null) moveToFallbackAetDir(proxyAEE, nSetFile, calledAET, reason); } File dstDir = new File(path.substring(0, path.indexOf(calledAET)) + proxyAEE.getFallbackDestinationAET()); dstDir.mkdir(); String fileName = file.getName(); File dst = new File(dstDir, fileName.substring(0, fileName.lastIndexOf(".")) + ".dcm"); if (file.renameTo(dst)) LOG.debug("Rename {} to {} {} and fallback AET is {}", new Object[] { file, dst, reason, proxyAEE.getFallbackDestinationAET() }); else LOG.error("Failed to rename {} to {}", new Object[] { file, dst }); File infoFile = new File(file.getParent(), fileName.substring(0, fileName.indexOf('.')) + ".info"); File infoDst = new File(dstDir, fileName.substring(0, fileName.indexOf('.')) + ".info"); if (infoFile.renameTo(infoDst)) LOG.debug("Rename {} to {} {} and fallback AET is {}", new Object[] { infoFile, infoDst, reason, proxyAEE.getFallbackDestinationAET() }); else LOG.error("Failed to rename {} to {}", new Object[] { infoFile, infoDst }); } private File getMatchingNsetFile(ProxyAEExtension proxyAEE, String calledAET, File file) throws IOException { File dir = new File(proxyAEE.getNSetDirectoryPath(), calledAET); if (!dir.exists()) return null; Properties nCreateProp = InfoFileUtils.getFileInfoProperties(proxyAEE, file); String sopInstanceUID = nCreateProp.getProperty("sop-instance-uid"); File[] nSetInfoFiles = dir.listFiles(InfoFileUtils.infoFileFilter()); for (File nSetInfoFile : nSetInfoFiles) { Properties nSetProp = InfoFileUtils.getFileInfoProperties(proxyAEE, nSetInfoFile); if (nSetProp.getProperty("sop-instance-uid").equals(sopInstanceUID)) return new File(nSetInfoFile.getParent(), nSetInfoFile.getName().substring(0, nSetInfoFile.getName().lastIndexOf('.')) + ".dcm"); } return null; } private boolean sendToFallbackAET(ProxyAEExtension proxyAEE, String destinationAET) { if (proxyAEE.getFallbackDestinationAET() != null) if (!destinationAET.equals(proxyAEE.getFallbackDestinationAET())) return true; return false; } protected Retry getMatchingRetry(ProxyAEExtension proxyAEE, String suffix) { for (Retry retry : proxyAEE.getRetries()) { String retrySuffix = retry.getRetryObject().getSuffix(); boolean startsWith = suffix.startsWith(retrySuffix); LOG.debug(">> \"{}\" starts with \"{}\" = {}", new Object[] { suffix, retrySuffix, startsWith }); if (startsWith) { LOG.debug("Found matching retry configuration: " + retry.getRetryObject()); return retry; } } LOG.debug("Found no matching retry configuration"); return null; } protected void moveToNoRetryPath(ProxyAEExtension proxyAEE, String calledAET, File file, String reason) throws IOException { String path = file.getPath(); if (path.contains("ncreate")) { File nSetFile = getMatchingNsetFile(proxyAEE, calledAET, file); if (nSetFile != null) moveToNoRetryPath(proxyAEE, calledAET, nSetFile, reason); } String spoolDirPath = proxyAEE.getSpoolDirectoryPath().getPath(); String fileName = file.getName(); String subPath = path.substring(path.indexOf(spoolDirPath) + spoolDirPath.length(), path.indexOf(fileName)); File dstDir = new File(proxyAEE.getNoRetryPath().getPath() + subPath); dstDir.mkdirs(); File dstFile = new File(dstDir, fileName); if (file.renameTo(dstFile)) LOG.debug("Rename {} to {} {} and fallback AET is {}", new Object[] { file, dstFile, reason, proxyAEE.getFallbackDestinationAET() }); else LOG.error("Failed to rename {} to {}", new Object[] { file, dstFile }); File infoFile = new File(file.getParent(), file.getName().substring(0, file.getName().indexOf('.')) + ".info"); File infoDst = new File(dstDir, fileName.substring(0, fileName.indexOf('.')) + ".info"); if (infoFile.renameTo(infoDst)) LOG.debug("Rename {} to {} {} and fallback AET is {}", new Object[] { infoFile, infoDst, reason, proxyAEE.getFallbackDestinationAET() }); else LOG.error("Failed to rename {} to {}", new Object[] { infoFile, infoDst }); File parentDir = file.getParentFile(); if (parentDir.list().length == 0) if (parentDir.delete()) LOG.debug("Delete empty dir {}", parentDir); else LOG.error("Error deleting dir {}", parentDir); } private void deleteFailedFile(ProxyAEExtension proxyAEE, String calledAET, File file, String reason, Integer retry) { try { String path = file.getPath(); Properties prop = InfoFileUtils.getFileInfoProperties(proxyAEE, file); if (proxyAEE.isEnableAuditLog() && path.contains("cstore")) { String callingAET = prop.getProperty("source-aet"); LogUtils.createStartLogFile(proxyAEE, AuditDirectory.DELETED, callingAET, calledAET, proxyAEE.getApplicationEntity().getConnections().get(0).getHostname(), prop, retry); LogUtils.writeLogFile(proxyAEE, AuditDirectory.DELETED, callingAET, calledAET, prop, file.length(), retry); } if (path.contains("ncreate")) deletePendingNSet(proxyAEE, calledAET, file, prop); if (file.delete()) LOG.debug("Delete {} {}", file, reason); else { LOG.error("Failed to delete {}", file); return; } File infoFile = new File(file.getParent(), file.getName().substring(0, file.getName().indexOf('.')) + ".info"); if (infoFile.delete()) LOG.debug("Delete {}", infoFile); else LOG.error("Failed to delete {}", infoFile); } catch (Exception e) { LOG.error("Failed to create log file: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } private void deletePendingNSet(ProxyAEExtension proxyAEE, String calledAET, File file, Properties prop) throws IOException { File nSetDir = new File(proxyAEE.getNSetDirectoryPath(), calledAET); if (nSetDir.exists() && nSetDir.list() != null && nSetDir.list().length != 0) { String sopInstanceUID = prop.getProperty("sop-instance-uid"); for (File nSetFile : nSetDir.listFiles(fileFilter(proxyAEE, calledAET))) { Properties nSetProp = InfoFileUtils.getFileInfoProperties(proxyAEE, nSetFile); if (nSetProp.getProperty("sop-instance-uid").equals(sopInstanceUID)) { if (nSetFile.delete()) LOG.debug("Delete {} before deleting matching N-CREATE file {}", nSetFile, file); else { LOG.error("Failed to delete {}", nSetFile); return; } File infoFile = new File(nSetFile.getParent(), nSetFile.getName().substring(0, nSetFile.getName().indexOf('.')) + ".info"); if (infoFile.delete()) LOG.debug("Delete {}", infoFile); else LOG.error("Failed to delete {}", infoFile); } } } } private void startForwardScheduledMPPS(final ProxyAEExtension proxyAEE, File[] files, final String destinationAETitle, final String protocol) { final File[] sendFiles = createSendFileList(files); try { forwardScheduledMPPS(proxyAEE, sendFiles, destinationAETitle, protocol); } catch (IOException e) { LOG.error("Error forwarding scheduled MPPS " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } private void resetSendFiles(File[] sendFiles) { for (File file : sendFiles) { String sndFileName = file.getPath(); File dst = new File(sndFileName.substring(0, sndFileName.length() - 4)); file.renameTo(dst); } } protected void forwardScheduledMPPS(ProxyAEExtension proxyAEE, File[] files, String destinationAETitle, String protocol) throws IOException { for (File file : files) { Properties prop = InfoFileUtils.getFileInfoProperties(proxyAEE, file); String callingAET = prop.containsKey("use-calling-aet") ? prop.getProperty("use-calling-aet") : prop.getProperty("source-aet"); try { if (protocol == "nset" && pendingNCreateForwarding(proxyAEE, destinationAETitle, file)) { String prevFilePath = file.getPath(); File dst = new File(prevFilePath.substring(0, prevFilePath.length() - 4)); if (file.renameTo(dst)) LOG.debug("{} has pending N-CREATE-RQ, rename to {}", prevFilePath, dst); else { LOG.error("Error renaming {} to {}.", prevFilePath, dst); } continue; } AAssociateRQ rq = new AAssociateRQ(); rq.addPresentationContext(new PresentationContext(1, UID.ModalityPerformedProcedureStepSOPClass, UID.ExplicitVRLittleEndian)); rq.setCallingAET(callingAET); rq.setCalledAET(destinationAETitle); Association as = proxyAEE.getApplicationEntity() .connect(aeCache.findApplicationEntity(destinationAETitle), rq); try { if (as.isReadyForDataTransfer()) { Attributes fmi = readFileMetaInformation(file); forwardScheduledMPPS(proxyAEE, as, file, fmi, protocol, prop); } else { renameFile(proxyAEE, RetryObject.ConnectionException.getSuffix(), file, destinationAETitle, prop); } } finally { if (as != null) { try { as.waitForOutstandingRSP(); as.release(); } catch (InterruptedException e) { LOG.error(as + ": unexpected exception: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } catch (IOException e) { LOG.error(as + ": failed to release association: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } } } catch (IOException e) { LOG.error("Error connecting to {}: {} ", destinationAETitle, e.getMessage()); renameFile(proxyAEE, RetryObject.ConnectionException.getSuffix(), file, destinationAETitle, prop); } catch (InterruptedException e) { LOG.error("Connection exception: " + e.getMessage()); renameFile(proxyAEE, RetryObject.ConnectionException.getSuffix(), file, destinationAETitle, prop); } catch (IncompatibleConnectionException e) { LOG.error("Incompatible connection: " + e.getMessage()); renameFile(proxyAEE, RetryObject.IncompatibleConnectionException.getSuffix(), file, destinationAETitle, prop); } catch (ConfigurationException e) { LOG.error("Unable to load configuration: " + e.getMessage()); renameFile(proxyAEE, RetryObject.ConfigurationException.getSuffix(), file, destinationAETitle, prop); } catch (GeneralSecurityException e) { LOG.error("Failed to create SSL context: " + e.getMessage()); renameFile(proxyAEE, RetryObject.GeneralSecurityException.getSuffix(), file, destinationAETitle, prop); } } } private void writeFailedAuditLogMessage(ProxyAEExtension proxyAEE, File file, Attributes fmi, String calledAET, Properties prop) throws IOException { if (proxyAEE.isEnableAuditLog() && file.getPath().contains("cstore")) { String sourceAET = prop.getProperty("source-aet"); int retry = getPreviousRetries(proxyAEE, file); LogUtils.createStartLogFile(proxyAEE, AuditDirectory.FAILED, sourceAET, calledAET, proxyAEE.getApplicationEntity().getConnections().get(0).getHostname(), prop, retry); LogUtils.writeLogFile(proxyAEE, AuditDirectory.FAILED, sourceAET, calledAET, prop, file.length(), retry); } } private boolean pendingNCreateForwarding(ProxyAEExtension proxyAEE, String destinationAETitle, File file) throws IOException { File dir = new File(proxyAEE.getNCreateDirectoryPath(), destinationAETitle); if (!dir.exists()) return false; Properties nSetProp = InfoFileUtils.getFileInfoProperties(proxyAEE, file); String sopInstanceUID = nSetProp.getProperty("sop-instance-uid"); File[] nCreateInfoFiles = dir.listFiles(InfoFileUtils.infoFileFilter()); for (File nCreateInfoFile : nCreateInfoFiles) { Properties nCreateProp = InfoFileUtils.getFileInfoProperties(proxyAEE, nCreateInfoFile); if (nCreateProp.getProperty("sop-instance-uid").equals(sopInstanceUID)) return true; } return false; } private void forwardScheduledMPPS(final ProxyAEExtension proxyAEE, final Association as, final File file, final Attributes fmi, String protocol, final Properties prop) throws IOException, InterruptedException { String iuid = fmi.getString(Tag.MediaStorageSOPInstanceUID); String cuid = fmi.getString(Tag.MediaStorageSOPClassUID); String tsuid = UID.ExplicitVRLittleEndian; DicomInputStream in = new DicomInputStream(file); Attributes attrs = in.readDataset(-1, -1); DimseRSPHandler rspHandler = new DimseRSPHandler(as.nextMessageID()) { @Override public void onDimseRSP(Association as, Attributes cmd, Attributes data) { super.onDimseRSP(as, cmd, data); int status = cmd.getInt(Tag.Status, -1); switch (status) { case Status.Success: LOG.debug("{}: forwarded file {} with status {}", new Object[] { as, file, Integer.toHexString(status) + 'H' }); deleteFile(as, file); break; default: { LOG.debug("{}: failed to forward file {} with error status {}", new Object[] { as, file, Integer.toHexString(status) + 'H' }); renameFile(proxyAEE, '.' + Integer.toHexString(status) + 'H', file, as.getCalledAET(), prop); } } } }; try { if (protocol == "ncreate") as.ncreate(cuid, iuid, attrs, tsuid, rspHandler); else as.nset(cuid, iuid, attrs, tsuid, rspHandler); } finally { in.close(); } } private void startForwardScheduledNAction(final ProxyAEExtension proxyAEE, final String destinationAETitle, File[] files) { final File[] sendFiles = createSendFileList(files); ((ProxyDeviceExtension) proxyAEE.getApplicationEntity().getDevice() .getDeviceExtension(ProxyDeviceExtension.class)).getFileForwardingExecutor() .execute(new Runnable() { @Override public void run() { try { forwardScheduledNAction(proxyAEE, destinationAETitle, sendFiles); } catch (IOException e) { LOG.error("Error forwarding scheduled NAction: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } }); } private void startForwardScheduledNEventReport(final ProxyAEExtension proxyAEE, File[] files) throws IOException { final File[] sendFiles = createSendFileList(files); final Properties prop = InfoFileUtils.getFileInfoProperties(proxyAEE, files[0]); ((ProxyDeviceExtension) proxyAEE.getApplicationEntity().getDevice() .getDeviceExtension(ProxyDeviceExtension.class)).getFileForwardingExecutor() .execute(new Runnable() { @Override public void run() { try { forwardScheduledNEventReport(proxyAEE, prop.getProperty("nevent-destination"), sendFiles); } catch (IOException e) { LOG.error("Error forwarding scheduled N-EVENT-REPORT-RQ: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } }); } private File[] createSendFileList(File[] files) { ArrayList<File> sendFilesList = new ArrayList<File>(); for (File file : files) { String prevFilePath = file.getPath(); File snd = new File(prevFilePath + ".snd"); if (file.renameTo(snd)) { LOG.debug("Rename {} to {}", prevFilePath, snd.getPath()); sendFilesList.add(snd); } else LOG.error("Error renaming {} to {}. Skip file for now and try again on next scheduler run.", prevFilePath, snd.getPath()); } return sendFilesList.toArray(new File[sendFilesList.size()]); } private void forwardScheduledNAction(ProxyAEExtension proxyAEE, String calledAET, File[] files) throws IOException { for (File file : files) { Properties prop = InfoFileUtils.getFileInfoProperties(proxyAEE, file); String callingAET = prop.containsKey("use-calling-aet") ? prop.getProperty("use-calling-aet") : prop.getProperty("source-aet"); try { DicomInputStream in = new DicomInputStream(file); Attributes attrs = in.readDataset(-1, -1); if (pendingCStoreFileForwarding(proxyAEE, calledAET, attrs)) continue; AAssociateRQ rq = new AAssociateRQ(); rq.addPresentationContext(new PresentationContext(1, UID.StorageCommitmentPushModelSOPClass, UID.ImplicitVRLittleEndian)); rq.addRoleSelection(new RoleSelection(prop.getProperty("sop-class-uid"), true, true)); rq.setCallingAET(callingAET); rq.setCalledAET(calledAET); Association asInvoked = null; try { asInvoked = proxyAEE.getApplicationEntity().connect(aeCache.findApplicationEntity(calledAET), rq); if (asInvoked.isReadyForDataTransfer()) { forwardScheduledNAction(proxyAEE, asInvoked, file, prop, attrs); } else { renameFile(proxyAEE, RetryObject.ConnectionException.getSuffix(), file, calledAET, prop); } } finally { in.close(); if (asInvoked != null) { try { asInvoked.waitForOutstandingRSP(); asInvoked.release(); } catch (InterruptedException e) { LOG.error(asInvoked + ": unexpected exception: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } catch (IOException e) { LOG.error(asInvoked + ": failed to release association: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } } } catch (InterruptedException e) { LOG.error(e.getMessage()); renameFile(proxyAEE, RetryObject.ConnectionException.getSuffix(), file, calledAET, prop); } catch (IncompatibleConnectionException e) { LOG.error(e.getMessage()); renameFile(proxyAEE, RetryObject.IncompatibleConnectionException.getSuffix(), file, calledAET, prop); } catch (ConfigurationException e) { LOG.error(e.getMessage()); renameFile(proxyAEE, RetryObject.ConfigurationException.getSuffix(), file, calledAET, prop); } catch (IOException e) { LOG.error(e.getMessage()); renameFile(proxyAEE, RetryObject.ConnectionException.getSuffix(), file, calledAET, prop); } catch (GeneralSecurityException e) { LOG.error(e.getMessage()); renameFile(proxyAEE, RetryObject.GeneralSecurityException.getSuffix(), file, calledAET, prop); } } } protected void forwardScheduledNEventReport(ProxyAEExtension proxyAEE, String calledAET, File[] files) throws IOException { for (File file : files) { Properties prop = InfoFileUtils.getFileInfoProperties(proxyAEE, file); String callingAET = prop.getProperty("use-calling-aet"); try { DicomInputStream in = new DicomInputStream(file); Attributes attrs = in.readDataset(-1, -1); AAssociateRQ rq = new AAssociateRQ(); rq.addPresentationContext(new PresentationContext(1, UID.StorageCommitmentPushModelSOPClass, UID.ImplicitVRLittleEndian)); rq.addRoleSelection(new RoleSelection(prop.getProperty("sop-class-uid"), true, true)); rq.setCallingAET(callingAET); LOG.info("Setting called AET to" + calledAET); rq.setCalledAET(calledAET); Association asInvoked = proxyAEE.getApplicationEntity() .connect(aeCache.findApplicationEntity(calledAET), rq); try { if (asInvoked.isReadyForDataTransfer()) { forwardScheduledNEventReport(proxyAEE, asInvoked, file, prop, attrs); } else { renameFile(proxyAEE, RetryObject.ConnectionException.getSuffix(), file, calledAET, prop); } } finally { in.close(); if (asInvoked != null) { try { asInvoked.waitForOutstandingRSP(); asInvoked.release(); } catch (InterruptedException e) { LOG.error(asInvoked + ": unexpected exception: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } catch (IOException e) { LOG.error(asInvoked + ": failed to release association: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } } } catch (InterruptedException e) { LOG.error(e.getMessage()); renameFile(proxyAEE, RetryObject.ConnectionException.getSuffix(), file, calledAET, prop); } catch (IncompatibleConnectionException e) { LOG.error(e.getMessage()); renameFile(proxyAEE, RetryObject.IncompatibleConnectionException.getSuffix(), file, calledAET, prop); } catch (ConfigurationException e) { LOG.error(e.getMessage()); renameFile(proxyAEE, RetryObject.ConfigurationException.getSuffix(), file, calledAET, prop); } catch (IOException e) { LOG.error(e.getMessage()); renameFile(proxyAEE, RetryObject.ConnectionException.getSuffix(), file, calledAET, prop); } catch (GeneralSecurityException e) { LOG.error(e.getMessage()); renameFile(proxyAEE, RetryObject.GeneralSecurityException.getSuffix(), file, calledAET, prop); } } } private boolean pendingCStoreFileForwarding(ProxyAEExtension proxyAEE, String calledAET, Attributes eventInfo) throws IOException { File dir = new File(proxyAEE.getCStoreDirectoryPath(), calledAET); if (!dir.exists()) return false; String[] files = dir.list(); Sequence referencedSOPSequence = eventInfo.getSequence(Tag.ReferencedSOPSequence); Iterator<Attributes> it = referencedSOPSequence.iterator(); while (it.hasNext()) { Attributes item = it.next(); String referencedSOPInstanceUID = item.getString(Tag.ReferencedSOPInstanceUID); for (String file : files) if (file.startsWith(referencedSOPInstanceUID)) return true; } return false; } private void forwardScheduledNAction(final ProxyAEExtension proxyAEE, final Association as, final File file, final Properties prop, final Attributes attrs) throws IOException, InterruptedException { String iuid = prop.getProperty("sop-instance-uid"); String cuid = prop.getProperty("sop-class-uid"); String tsuid = UID.ImplicitVRLittleEndian; String transactionUID = attrs.getString(Tag.TransactionUID); final File destDir = new File(proxyAEE.getNeventDirectoryPath() + proxyAEE.getSeparator() + transactionUID + proxyAEE.getSeparator() + as.getCalledAET()); destDir.mkdirs(); String fileName = file.getName(); File dest = new File(destDir, fileName.substring(0, fileName.lastIndexOf('.')) + ".naction"); try { StgCmt.copyFile(as, file, destDir, dest); } catch (IOException e) { LOG.error("{}: could not Copy Naction files to NEventReportDir: {}", new Object[] { as, e }); if (LOG.isDebugEnabled()) e.printStackTrace(); } final File nactionDir = file.getParentFile(); final File neventDir = destDir.getParentFile(); DimseRSPHandler rspHandler = new DimseRSPHandler(as.nextMessageID()) { @Override public void onDimseRSP(Association asInvoked, Attributes cmd, Attributes data) { super.onDimseRSP(asInvoked, cmd, data); int status = cmd.getInt(Tag.Status, -1); switch (status) { case Status.Success: { String callingAET = asInvoked.getAAssociateRQ().getCallingAET(); String proxyAET = asInvoked.getApplicationEntity().getAETitle(); if (callingAET != null && callingAET.equals(proxyAET)) { // n-event-report-rq will come to this proxy AET // keep the files copied earlier in the nevent directory // just remove the files in the naction directory boolean deletedNactionDir = FileUtils.deleteQuietly(file.getParentFile()); if (deletedNactionDir) LOG.debug("{}: DELETE N-ACTION-RQ {}", new Object[] { asInvoked, nactionDir.getPath() }); else LOG.error("{}: failed to DELETE N-ACTION-RQ {}", new Object[] { asInvoked, nactionDir.getPath() }); } else { // n-event-report-rq will not come back to this proxy // remove the files from the naction and the nevent LOG.debug( "{}: delete forwarded N-ACTION-RQ and Copy, due to Calling AET ({}) != Proxy AET ({})", new Object[] { as, callingAET, proxyAET }); boolean deletedNactionDir = FileUtils.deleteQuietly(file.getParentFile()); boolean deletedNeventDir = FileUtils.deleteQuietly(destDir); if (deletedNactionDir) LOG.debug("{}: DELETE {}", new Object[] { as, nactionDir.getPath() }); else LOG.error("{}: failed to DELETE {}", new Object[] { as, nactionDir.getPath() }); if (deletedNeventDir) LOG.debug("{}: DELETE {}", new Object[] { as, neventDir.getPath() }); else { LOG.error("{}: failed to DELETE {}", new Object[] { as, neventDir.getPath() }); } } break; } default: { LOG.error("{}: failed to forward N-ACTION file {} with error status {}", new Object[] { as, file, Integer.toHexString(status) + 'H' }); renameFile(proxyAEE, '.' + Integer.toHexString(status) + 'H', file, as.getCalledAET(), prop); boolean deletedNeventDir = FileUtils.deleteQuietly(destDir); if (deletedNeventDir) LOG.debug("{}: DELETE {}", new Object[] { as, neventDir.getPath() }); else { LOG.error("{}: failed to DELETE {}", new Object[] { as, neventDir.getPath() }); } } } } }; as.naction(cuid, iuid, 1, attrs, tsuid, rspHandler); } private void forwardScheduledNEventReport(final ProxyAEExtension proxyAEE, final Association as, final File file, final Properties prop, Attributes attrs) throws IOException, InterruptedException { String iuid = prop.getProperty("sop-instance-uid"); String cuid = prop.getProperty("sop-class-uid"); String tsuid = UID.ImplicitVRLittleEndian; int eventTypeId = attrs.contains(Tag.FailedSOPSequence) ? 2 : 1; DimseRSPHandler rspHandler = new DimseRSPHandler(as.nextMessageID()) { @Override public void onDimseRSP(Association asInvoked, Attributes cmd, Attributes data) { super.onDimseRSP(asInvoked, cmd, data); int status = cmd.getInt(Tag.Status, -1); switch (status) { case Status.Success: { LOG.debug("{}: successfully forwarded N-EVENT-REPORT file {} to {}", new Object[] { as, file, as.getRemoteAET() }); deleteFile(as, file); break; } default: { LOG.error("{}: failed to forward N-EVENT-REPORT-RQ file {} with error status {}", new Object[] { as, file, Integer.toHexString(status) + 'H' }); renameFile(proxyAEE, '.' + Integer.toHexString(status) + 'H', file, as.getCalledAET(), prop); } } } }; as.neventReport(cuid, iuid, eventTypeId, attrs, tsuid, rspHandler); } private void startForwardScheduledCStoreFiles(final ProxyAEExtension proxyAEE, final String calledAET, final File[] files) { ((ProxyDeviceExtension) proxyAEE.getApplicationEntity().getDevice() .getDeviceExtension(ProxyDeviceExtension.class)).getFileForwardingExecutor() .execute(new Runnable() { @Override public void run() { forwardScheduledCStoreFiles(proxyAEE, calledAET, files); } }); } private void forwardScheduledCStoreFiles(ProxyAEExtension proxyAEE, String calledAET, File[] files) { Collection<ForwardTask> forwardTasks = null; forwardTasks = scanFiles(proxyAEE, calledAET, files); for (ForwardTask ft : forwardTasks) try { processForwardTask(proxyAEE, ft); } catch (IOException e) { LOG.error("Error processing forwarding files: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } private void processForwardTask(ProxyAEExtension proxyAEE, ForwardTask ft) throws IOException { AAssociateRQ rq = ft.getAAssociateRQ(); Association asInvoked = null; Properties prop = InfoFileUtils.getFileInfoProperties(proxyAEE, ft.getFiles().get(0)); try { if (proxyAEE.getForwardOptions().containsKey(rq.getCalledAET()) && proxyAEE.getForwardOptions().get(rq.getCalledAET()).isConvertEmf2Sf()) ForwardConnectionUtils.addReducedTS(rq); asInvoked = proxyAEE.getApplicationEntity().connect(aeCache.findApplicationEntity(rq.getCalledAET()), rq); for (File file : ft.getFiles()) { prop = InfoFileUtils.getFileInfoProperties(proxyAEE, file); try { String cuid = prop.getProperty("sop-class-uid"); if (ForwardConnectionUtils.requiresMultiFrameConversion(proxyAEE, asInvoked.getCalledAET(), cuid)) processEmf2Sf(proxyAEE, asInvoked, prop, file); else if (asInvoked.isReadyForDataTransfer()) { Attributes attrs = proxyAEE.parseAttributesWithLazyBulkData(asInvoked, file); AttributeCoercion ac = proxyAEE.getAttributeCoercion(asInvoked.getCalledAET(), cuid, Role.SCU, Dimse.C_STORE_RQ); if (ac != null) attrs = AttributeCoercionUtils.coerceAttributes(asInvoked, proxyAEE, attrs, ac); forwardScheduledCStoreFile(proxyAEE, asInvoked, new DataWriterAdapter(attrs), -1, file, prop, file.length()); } else renameFile(proxyAEE, RetryObject.ConnectionException.getSuffix(), file, rq.getCalledAET(), prop); } catch (NoPresentationContextException npc) { handleForwardException(proxyAEE, asInvoked, file, npc, RetryObject.NoPresentationContextException.getSuffix(), prop, true); } catch (AssociationStateException ass) { handleForwardException(proxyAEE, asInvoked, file, ass, RetryObject.AssociationStateException.getSuffix(), prop, true); } catch (IOException ioe) { handleForwardException(proxyAEE, asInvoked, file, ioe, RetryObject.ConnectionException.getSuffix(), prop, true); } catch (Exception e) { LOG.error("Unexpected exception: ", e.getMessage()); handleForwardException(proxyAEE, asInvoked, file, e, RetryObject.Exception.getSuffix(), prop, true); } } } catch (ConfigurationException ce) { handleProcessForwardTaskException(proxyAEE, rq, ft, ce, RetryObject.ConfigurationException.getSuffix(), prop); } catch (AAssociateRJ rj) { handleProcessForwardTaskException(proxyAEE, rq, ft, rj, RetryObject.AAssociateRJ.getSuffix(), prop); } catch (AAbort aa) { handleProcessForwardTaskException(proxyAEE, rq, ft, aa, RetryObject.AAbort.getSuffix(), prop); } catch (IOException e) { handleProcessForwardTaskException(proxyAEE, rq, ft, e, RetryObject.ConnectionException.getSuffix(), prop); } catch (InterruptedException e) { handleProcessForwardTaskException(proxyAEE, rq, ft, e, RetryObject.ConnectionException.getSuffix(), prop); } catch (IncompatibleConnectionException e) { handleProcessForwardTaskException(proxyAEE, rq, ft, e, RetryObject.IncompatibleConnectionException.getSuffix(), prop); } catch (GeneralSecurityException e) { handleProcessForwardTaskException(proxyAEE, rq, ft, e, RetryObject.GeneralSecurityException.getSuffix(), prop); } finally { if (asInvoked != null) { try { asInvoked.waitForOutstandingRSP(); asInvoked.release(); } catch (InterruptedException e) { LOG.error(asInvoked + ": unexpected exception: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } catch (IOException e) { LOG.error(asInvoked + ": failed to release association: " + e.getMessage()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } } } private void processEmf2Sf(ProxyAEExtension proxyAEE, Association asInvoked, Properties prop, File file) throws IOException, InterruptedException { Attributes src; DicomInputStream dis = new DicomInputStream(file); try { dis.setIncludeBulkData(IncludeBulkData.URI); src = dis.readDataset(-1, -1); } finally { dis.close(); } MultiframeExtractor extractor = new MultiframeExtractor(); int n = src.getInt(Tag.NumberOfFrames, 1); long t = 0; boolean log = true; for (int frameNumber = n - 1; frameNumber >= 0; --frameNumber) { long t1 = System.currentTimeMillis(); Attributes attrs = extractor.extract(src, frameNumber); long t2 = System.currentTimeMillis(); t = t + t2 - t1; long length = attrs.calcLength(DicomEncodingOptions.DEFAULT, true); if (asInvoked.isReadyForDataTransfer()) { prop.setProperty("sop-instance-uid", attrs.getString(Tag.SOPInstanceUID)); prop.setProperty("sop-class-uid", attrs.getString(Tag.SOPClassUID)); forwardScheduledCStoreFile(proxyAEE, asInvoked, new DataWriterAdapter(attrs), frameNumber, file, prop, length); } else { log = false; break; } } if (log) LOG.info("{}: extracted {} frames from multi-frame object {} in {}sec", new Object[] { asInvoked, n, src.getString(Tag.SOPInstanceUID), t / 1000F }); } private void forwardScheduledCStoreFile(final ProxyAEExtension proxyAEE, final Association asInvoked, DataWriterAdapter data, final int frame, final File file, final Properties prop, final long fileSize) throws IOException, InterruptedException { final String cuid = prop.getProperty("sop-class-uid"); final String iuid = prop.getProperty("sop-instance-uid"); final String tsuid = prop.getProperty("transfer-syntax-uid"); DimseRSPHandler rspHandler = new DimseRSPHandler(asInvoked.nextMessageID()) { @Override public void onDimseRSP(Association asInvoked, Attributes cmd, Attributes data) { super.onDimseRSP(asInvoked, cmd, data); int status = cmd.getInt(Tag.Status, -1); switch (status) { case Status.Success: case Status.CoercionOfDataElements: { if (proxyAEE.isEnableAuditLog()) LogUtils.writeLogFile(proxyAEE, AuditDirectory.TRANSFERRED, asInvoked.getCallingAET(), asInvoked.getRemoteAET(), prop, fileSize, -1); if (frame > 0) return; deleteFile(asInvoked, file); break; } default: { LOG.debug("{}: failed to forward file {} with error status {}", new Object[] { asInvoked, file, Integer.toHexString(status) + 'H' }); try { renameFile(proxyAEE, '.' + Integer.toHexString(status) + 'H', file, asInvoked.getCalledAET(), prop); } catch (Exception e) { LOG.error("{}: error renaming file {}: {}", new Object[] { asInvoked, file.getPath(), e.getMessage() }); if (LOG.isDebugEnabled()) e.printStackTrace(); } } } } }; if (proxyAEE.isEnableAuditLog()) { String sourceAET = prop.getProperty("source-aet"); LogUtils.createStartLogFile(proxyAEE, AuditDirectory.TRANSFERRED, sourceAET, asInvoked.getRemoteAET(), asInvoked.getConnection().getHostname(), prop, 0); } String ts = ForwardConnectionUtils.getMatchingTsuid(asInvoked, tsuid, cuid); asInvoked.cstore(cuid, iuid, 0, data, ts, rspHandler); } private Integer getPreviousRetries(ProxyAEExtension proxyAEE, File file) { String suffix = file.getName().substring(file.getName().lastIndexOf('.')); Retry matchingRetry = getMatchingRetry(proxyAEE, suffix); if (matchingRetry != null) { String substring = suffix.substring(matchingRetry.getRetryObject().getSuffix().length()); if (!substring.isEmpty()) try { return Integer.parseInt(substring); } catch (NumberFormatException e) { LOG.error("Error parsing number of retries in suffix of file " + file.getName()); if (LOG.isDebugEnabled()) e.printStackTrace(); } } return 1; } private Collection<ForwardTask> scanFiles(ProxyAEExtension proxyAEE, String calledAET, File[] files) { HashMap<String, ForwardTask> map = new HashMap<String, ForwardTask>(4); for (File file : files) { try { if (lock.tryLock(100, TimeUnit.MILLISECONDS)) { try { if (file.exists()) { String prevFilePath = file.getPath(); File snd = new File(prevFilePath + ".snd"); try { Files.move(file.toPath(), snd.toPath(), StandardCopyOption.REPLACE_EXISTING); LOG.debug("Successfully renamed {} to {}", prevFilePath, snd.getPath()); LOG.debug("Adding file {} to forward tasks ", snd.getPath()); addFileToFwdTaskMap(proxyAEE, calledAET, snd, map); LOG.debug( "Successfully added file {} to forward tasks , proceeding with scheduled send", snd.getPath()); } catch (Exception e) { LOG.error( "Error moving {} to {}. Skip file for now and try again on next scheduler run. - {}", prevFilePath, snd.getPath(), e); if (!file.exists() && snd.exists()) if (snd.renameTo(file)) LOG.debug("Rename {} to {}", snd.getPath(), file.getPath()); else LOG.debug("Error renaming {} to {}", snd.getPath(), file.getPath()); else if (snd.exists() && file.exists()) try { Files.delete(snd.toPath()); } catch (Exception e1) { LOG.error( "Unable to delete file {} after failed rename from {} to {} - {}", snd, prevFilePath, snd.getPath(), e1); } } } } finally { lock.unlock(); } } } catch (InterruptedException e) { LOG.error("Error acquiring lock for file scan and rename {}", e); } } return map.values(); } private void addFileToFwdTaskMap(ProxyAEExtension proxyAEE, String calledAET, File file, HashMap<String, ForwardTask> map) throws IOException { Properties prop = InfoFileUtils.getFileInfoProperties(proxyAEE, file); String callingAET = prop.containsKey("use-calling-aet") ? prop.getProperty("use-calling-aet") : prop.getProperty("source-aet"); String cuid = prop.getProperty("sop-class-uid"); String tsuid = prop.getProperty("transfer-syntax-uid"); ForwardTask forwardTask = map.get(callingAET); if (forwardTask == null) { LOG.debug("Creating new forward task for Calling AET {} and Called AET {}", callingAET, calledAET); forwardTask = new ForwardTask(callingAET, calledAET); map.put(callingAET, forwardTask); } else { LOG.debug("Loaded forward task for Calling AET {} and Called AET {}", callingAET, forwardTask.getAAssociateRQ().getCalledAET()); } LOG.debug( "Add file {} to forward task for Calling AET {} and Called AET {} with SOP Class UID = {} and Transfer Syntax UID = {}", new Object[] { file.getPath(), callingAET, forwardTask.getAAssociateRQ().getCalledAET(), cuid, tsuid }); forwardTask.addFile(file, cuid, tsuid); } private static Attributes readFileMetaInformation(File file) throws IOException { DicomInputStream in = new DicomInputStream(file); try { return in.readFileMetaInformation(); } finally { in.close(); } } private void handleForwardException(ProxyAEExtension proxyAEE, Association as, File file, Exception e, String suffix, Properties prop, boolean writeAuditLogMessage) throws IOException { LOG.error(as + ": error processing forward task: " + e.getMessage()); as.setProperty(ProxyAEExtension.FILE_SUFFIX, suffix); renameFile(proxyAEE, suffix, file, as.getCalledAET(), prop); if (LOG.isDebugEnabled()) e.printStackTrace(); } private void handleProcessForwardTaskException(ProxyAEExtension proxyAEE, AAssociateRQ rq, ForwardTask ft, Exception e, String suffix, Properties prop) throws IOException { LOG.error("Unable to connect to {}: {}", new Object[] { ft.getAAssociateRQ().getCalledAET(), e.getMessage() }); for (File file : ft.getFiles()) { renameFile(proxyAEE, suffix, file, rq.getCalledAET(), prop); } } private void renameFile(ProxyAEExtension proxyAEE, String suffix, File file, String calledAET, Properties prop) { File dst; String path = file.getPath(); if (path.endsWith(".snd")) dst = setFileSuffix(path.substring(0, path.length() - 4), suffix); else dst = setFileSuffix(path, suffix); if (file.renameTo(dst)) { dst.setLastModified(System.currentTimeMillis()); LOG.debug("Rename {} to {}", new Object[] { file, dst }); try { writeFailedAuditLogMessage(proxyAEE, dst, null, calledAET, prop); } catch (IOException e) { LOG.error("Failed to write audit log message"); if (LOG.isDebugEnabled()) e.printStackTrace(); } } else { LOG.error("Failed to rename {} to {}", new Object[] { file, dst }); } } private File setFileSuffix(String path, String newSuffix) { int indexOfNewSuffix = path.lastIndexOf(newSuffix); if (indexOfNewSuffix == -1) return new File(path + newSuffix + "1"); int indexOfNumRetries = indexOfNewSuffix + newSuffix.length(); int indexOfNextSuffix = path.indexOf('.', indexOfNewSuffix + 1); if (indexOfNextSuffix == -1) { String substring = path.substring(indexOfNumRetries); int previousNumRetries = Integer.parseInt(substring); return new File(path.substring(0, indexOfNumRetries) + Integer.toString(previousNumRetries + 1)); } int previousNumRetries = Integer.parseInt(path.substring(indexOfNumRetries, indexOfNextSuffix)); String substringStart = path.substring(0, indexOfNewSuffix); String substringEnd = path.substring(indexOfNextSuffix); String pathname = substringStart + substringEnd + newSuffix + Integer.toString(previousNumRetries + 1); return new File(pathname); } private static void deleteFile(Association as, File file) { try { Files.delete(file.toPath()); LOG.debug("{}: delete {}", as, file); } catch (Exception e) { LOG.debug("{}: failed to delete {} - {}", as, file, e); } File infoFile = new File(file.getParent(), file.getName().substring(0, file.getName().indexOf('.')) + ".info"); try { Files.delete(infoFile.toPath()); LOG.debug("{}: delete {}", as, infoFile); } catch (Exception e) { LOG.debug("{}: failed to delete {} - {}", as, infoFile, e); } File path = new File(file.getParent()); if (path != null && path.list() != null && path.list().length == 0) path.delete(); } private static void deleteFile(File file) { try { Files.delete(file.toPath()); LOG.debug("Delete {}", file); } catch (Exception e) { LOG.debug("Failed to delete {} - {}", file, e); } File infoFile = new File(file.getParent(), file.getName().substring(0, file.getName().indexOf('.')) + ".info"); try { Files.delete(infoFile.toPath()); LOG.debug("Delete {}", infoFile); } catch (Exception e) { LOG.debug("Failed to delete {} - {}", infoFile, e); } File path = new File(file.getParent()); if (path.list().length == 0) path.delete(); } public static boolean deleteFolder(File directory) { if (directory.exists()) { File[] files = directory.listFiles(); if (files != null) { for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { deleteFolder(files[i]); } else { files[i].delete(); } } } } return (directory.delete()); } }