org.dcm4che3.tool.unvscp.UnvSCP.java Source code

Java tutorial

Introduction

Here is the source code for org.dcm4che3.tool.unvscp.UnvSCP.java

Source

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in
 * Java(TM), hosted at https://github.com/gunterze/dcm4che.
 *
 * The Initial Developer of the Original Code is
 * Agfa Healthcare.
 * Portions created by the Initial Developer are Copyright (C) 2011
 * 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.dcm4che3.tool.unvscp;

import com.google.gson.Gson;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.MissingOptionException;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.log4j.LogManager;
import org.apache.log4j.PropertyConfigurator;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.tool.unvscp.data.DicomAttribute;
import org.dcm4che3.data.Sequence;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.UID;
import org.dcm4che3.data.VR;
import org.dcm4che3.io.DicomInputStream;
import org.dcm4che3.io.DicomOutputStream;
import org.dcm4che3.io.DicomInputStream.IncludeBulkData;
import org.dcm4che3.media.DicomDirWriter;
import org.dcm4che3.tool.unvscp.media.DicomWebReader;
import org.dcm4che3.tool.unvscp.media.IDicomReader;
import org.dcm4che3.media.RecordFactory;
import org.dcm4che3.media.RecordType;
import org.dcm4che3.net.ApplicationEntity;
import org.dcm4che3.net.Association;
import org.dcm4che3.net.AssociationHandler;
import org.dcm4che3.net.AssociationStateException;
import org.dcm4che3.net.Commands;
import org.dcm4che3.net.Connection;
import org.dcm4che3.net.Device;
import org.dcm4che3.net.Dimse;
import org.dcm4che3.net.IncompatibleConnectionException;
import org.dcm4che3.net.PDVInputStream;
import org.dcm4che3.net.QueryOption;
import org.dcm4che3.net.Status;
import org.dcm4che3.net.TransferCapability;
import org.dcm4che3.net.pdu.AAssociateAC;
import org.dcm4che3.net.pdu.AAssociateRQ;
import org.dcm4che3.net.pdu.ExtendedNegotiation;
import org.dcm4che3.net.pdu.PresentationContext;
import org.dcm4che3.net.pdu.RoleSelection;
import org.dcm4che3.net.service.AbstractDicomService;
import org.dcm4che3.net.service.BasicCEchoSCP;
import org.dcm4che3.net.service.BasicCFindSCP;
import org.dcm4che3.net.service.BasicCGetSCP;
import org.dcm4che3.net.service.BasicCMoveSCP;
import org.dcm4che3.net.service.BasicCStoreSCP;
import org.dcm4che3.net.service.BasicRetrieveTask;
import org.dcm4che3.net.service.DicomServiceException;
import org.dcm4che3.net.service.DicomServiceRegistry;
import org.dcm4che3.net.service.InstanceLocator;
import org.dcm4che3.net.service.QueryRetrieveLevel;
import org.dcm4che3.net.service.QueryTask;
import org.dcm4che3.net.service.RetrieveTask;
import org.dcm4che3.tool.common.CLIUtils;
import org.dcm4che3.tool.common.FilesetInfo;
import org.dcm4che3.tool.unvscp.data.CAuthWebResponse;
import org.dcm4che3.tool.unvscp.data.CMoveWebResponse;
import org.dcm4che3.tool.unvscp.data.CStoreWebResponse;
import org.dcm4che3.tool.unvscp.data.DicomFileDataPresentation;
import org.dcm4che3.tool.unvscp.data.DicomFileWebData;
import org.dcm4che3.tool.unvscp.data.GenericWebResponse;
import org.dcm4che3.tool.unvscp.gui.ActivityWindow;
import org.dcm4che3.tool.unvscp.media.DicomClientMetaInfo;
import org.dcm4che3.tool.unvscp.media.DicomDirReaderWrapper;
import org.dcm4che3.tool.unvscp.media.DicomDirWriterWrapper;
import org.dcm4che3.tool.unvscp.media.IDicomWriter;
import org.dcm4che3.tool.unvscp.media.UnvWebClient;
import org.dcm4che3.tool.unvscp.media.UnvWebClientListener;
import org.dcm4che3.tool.unvscp.net.pdu.UnvAAssociateRJ;
import org.dcm4che3.tool.unvscp.tasks.AsyncSenderTask;
import org.dcm4che3.tool.unvscp.tasks.DcmFileSenderTask;
import org.dcm4che3.util.AttributesFormat;
import org.dcm4che3.util.SafeClose;
import org.dcm4che3.util.StringUtils;
import org.dcm4che3.util.TagUtils;
import org.dcm4che3.util.UIDUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Gunter Zeilinger <gunterze@gmail.com> Pavel Varzinov <varzinov@yandex.ru> Alexander Sirotin <alexander.sirotin@unvsoft.com>
 *
 */
public class UnvSCP implements UnvWebClientListener {

    static final Logger LOG = LoggerFactory.getLogger(UnvSCP.class);
    private static boolean isDynamicAEList = false;

    private static final String[] PATIENT_ROOT_LEVELS = { "PATIENT", "STUDY", "SERIES", "IMAGE" };
    private static final String[] STUDY_ROOT_LEVELS = { "STUDY", "SERIES", "IMAGE" };
    private static final String[] PATIENT_STUDY_ONLY_LEVELS = { "PATIENT", "STUDY" };
    private static ResourceBundle rb = ResourceBundle.getBundle("org.dcm4che3.tool.unvscp.messages");

    private final Device device = new Device("unvscp");
    private final ApplicationEntity ae = new ApplicationEntity("*");
    private final Connection conn = new Connection();

    private File storageDir;
    private File dicomDir;
    private boolean isUrl = false;
    private boolean notConcurrent = false;
    private URL dicomUrl, dicomDownloadUrl;
    private String emsowUsername, emsowPassword;
    private AttributesFormat filePathFormat;
    private RecordFactory recFact;
    private String availability;
    private boolean stgCmtOnSameAssoc;
    private boolean sendPendingCGet;
    private int sendPendingCMoveInterval;
    private final FilesetInfo fsInfo = new FilesetInfo();
    private IDicomReader ddReader;
    private IDicomWriter ddWriter;
    private final Map<String, Connection> remoteConnections = Collections
            .synchronizedMap(new HashMap<String, Connection>());
    private CAuthWebResponse.AERecord emsowServerAeMeta;
    private final Map<String, CAuthWebResponse.AERecord> emsowClientAets = Collections
            .synchronizedMap(new HashMap<String, CAuthWebResponse.AERecord>());
    private final Set<ExtendedNegotiation> extendedNegotiaions = new LinkedHashSet<ExtendedNegotiation>();
    private final Set<RoleSelection> roleSelections = new LinkedHashSet<RoleSelection>();
    private boolean isPushForUnknownEnabled, isPullForUnknownEnabled;
    private boolean allowUnknown = false;
    private boolean async = false;
    private int sleepTime = 5;
    private File badFilesDir;
    private File uplDir, uplBadFilesDir;
    private File tmpDir;
    private int uplSleepTime = 5;
    private Integer compressionLevel = null; // if null then compression is not used

    private Thread asyncSender, dcmFileSender;

    private ActivityWindow activityWindow;

    private final class CStoreSCPImpl extends BasicCStoreSCP {

        CStoreSCPImpl() {
            super("*");
        }

        @Override
        protected void store(Association as, PresentationContext pc, Attributes rq, PDVInputStream data,
                Attributes rsp) throws IOException {
            String cuid = rq.getString(Tag.AffectedSOPClassUID);
            String iuid = rq.getString(Tag.AffectedSOPInstanceUID);
            String tsuid = pc.getTransferSyntax();
            File file = new File(storageDir, iuid);
            try {
                Attributes fmi = as.createFileMetaInformation(iuid, cuid, tsuid);
                storeTo(as, fmi, data, file, iuid);
                Attributes attrs = parse(file);
                File dest = getDestinationFile(attrs);
                renameTo(as, file, dest);
                file = dest;
                if (addDicomDirRecords(as, attrs, fmi, file)) {
                    LOG.info("{}: M-UPDATE {}", as, dicomDir);
                } else {
                    LOG.info("{}: ignore received object", as);
                    deleteFile(as, file, iuid);
                }

            } catch (Exception e) {
                deleteFile(as, file, iuid);
                throw new DicomServiceException(Status.ProcessingFailure, e);
            }
        }
    };

    private final class CStoreWebSCPImpl extends BasicCStoreSCP {
        CStoreWebSCPImpl() {
            super("*");
        }

        private void onAddNewFile(DicomFileDataPresentation dfdp) {
            if (activityWindow != null && dfdp != null) {
                activityWindow.onAddNewFile(dfdp.getSopInstanceUid(), dfdp.getStudyInstanceUid(),
                        dfdp.getStudyDate(), dfdp.getStudyDescription(), dfdp.getPatientName(),
                        dfdp.getPatientBirthDate());
            }
        }

        private void onStartProcessingFile(DicomFileDataPresentation dfdp) {
            if (activityWindow != null && dfdp != null) {
                activityWindow.onStartProcessingFile(dfdp.getSopInstanceUid());
            }
        }

        private void onFinishProcessingFile(DicomFileDataPresentation dfdp, String errMsg) {
            if (activityWindow != null && dfdp != null) {
                activityWindow.onFinishProcessingFile(dfdp.getSopInstanceUid(), errMsg);
            }
        }

        @Override
        protected void store(Association as, PresentationContext pc, Attributes rq, PDVInputStream data,
                Attributes rsp) throws IOException {

            String serviceCommand = "C-STORE";
            checkEmsowPermissions(as, serviceCommand);

            String iuid = rq.getString(Tag.AffectedSOPInstanceUID);
            String cuid = rq.getString(Tag.AffectedSOPClassUID);
            String tsuid = pc.getTransferSyntax();
            Attributes fmi = as.createFileMetaInformation(iuid, cuid, tsuid);

            String fileName = java.util.UUID.randomUUID().toString();
            if (UnvSCP.this.async) {
                fileName = (new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss.SSS-")).format(new Date()) + fileName;
            }

            File file = new File(UnvSCP.this.storageDir, fileName);

            FileOutputStream fos = null;
            File metaFile = null;
            try {
                storeTo(as, fmi, data, file, iuid);

                DicomFileDataPresentation dicomFileData = null;
                try {
                    dicomFileData = new DicomFileDataPresentation(file);
                } catch (IOException ioe) {
                }
                onAddNewFile(dicomFileData);

                if (UnvSCP.this.async) {
                    Properties metaData = UnvWebClient.getAssocParams(as);
                    if (metaData != null) {
                        metaData.setProperty("SOP_INSTANCE_UID", iuid);
                        metaFile = new File(UnvSCP.this.storageDir, file.getName() + ".1.meta");
                        fos = new FileOutputStream(metaFile);
                        metaData.store(fos, null);
                        fos.close();
                    } else {
                        LOG.warn("{}: M-WRITE-BEGIN metafile was not created due to empty Association data, UID={}",
                                new Object[] { as, iuid });
                    }
                    file.renameTo(new File(file.getParentFile(), file.getName() + ".2.dcm"));
                    Thread asyncSender = UnvSCP.this.asyncSender;
                    if (asyncSender != null && asyncSender.isAlive()) {
                        synchronized (asyncSender) {
                            asyncSender.notify();
                        }
                    }
                } else {
                    onStartProcessingFile(dicomFileData);
                    try {
                        // We rename the file because it must have extension ".dcm"
                        File dcmFile = new File(file.getParentFile(), file.getName() + ".dcm");
                        if (file.renameTo(dcmFile)) {
                            file = dcmFile;
                        }
                        this.doWebCStore(as, serviceCommand, iuid, file);
                        onFinishProcessingFile(dicomFileData, null);
                    } catch (IOException ioe) {
                        onFinishProcessingFile(dicomFileData, ioe.getMessage());
                        throw ioe;
                    }
                }
            } catch (Exception e) {
                try {
                    fos.close();
                } catch (Exception ignore) {
                }
                try {
                    metaFile.delete();
                } catch (Exception ignore) {
                }
                throw new DicomServiceException(Status.OutOfResources, e.getMessage());
            } finally {
                if (file.exists()) {
                    deleteFile(as, file, iuid);
                }
            }
        }

        private void doWebCStore(Association as, String serviceCommand, String iuid, File file) throws IOException {
            DicomClientMetaInfo metaInfo = new DicomClientMetaInfo(as);
            UnvWebClient webClient = new UnvWebClient(UnvSCP.this.dicomUrl.toString());
            webClient.addListener(UnvSCP.this, metaInfo);
            webClient.setBasicParams(UnvSCP.this.emsowUsername, UnvSCP.this.emsowPassword, serviceCommand, as);
            webClient.setParam("SOP_INSTANCE_UID", iuid);
            webClient.attachFile("file", file);

            if (UnvSCP.this.notConcurrent) {
                synchronized (UnvSCP.this) {
                    webClient.uploadFiles();
                }
            } else {
                webClient.uploadFiles();
            }
            CStoreWebResponse cswr = webClient.parseJsonResponse(CStoreWebResponse.class);

            Collection<CStoreWebResponse.ExtraMessage> result = cswr.getData();

            if (result != null) {
                for (CStoreWebResponse.ExtraMessage rec : result) {
                    if (rec.getSuccess() == null) {
                        rec.setSuccess("1");
                    }
                    if (rec.getDebug() == null) {
                        rec.setDebug("0");
                    }
                    if (rec.getDebugLevel() == null) {
                        rec.setDebugLevel("0");
                    }
                    logMessage(metaInfo, serviceCommand, rec, "EMSOW-DIALOG");
                }
            }
        }

    }

    private final class StgCmtSCPImpl extends AbstractDicomService {

        public StgCmtSCPImpl() {
            super(UID.StorageCommitmentPushModelSOPClass);
        }

        @Override
        public void onDimseRQ(Association as, PresentationContext pc, Dimse dimse, Attributes rq,
                Attributes actionInfo) throws IOException {
            if (dimse != Dimse.N_ACTION_RQ)
                throw new DicomServiceException(Status.UnrecognizedOperation);

            int actionTypeID = rq.getInt(Tag.ActionTypeID, 0);
            if (actionTypeID != 1)
                throw new DicomServiceException(Status.NoSuchActionType).setActionTypeID(actionTypeID);

            Attributes rsp = Commands.mkNActionRSP(rq, Status.Success);
            String callingAET = as.getCallingAET();
            String calledAET = as.getCalledAET();
            Connection remoteConnection = getRemoteConnection(callingAET);
            if (remoteConnection == null)
                throw new DicomServiceException(Status.ProcessingFailure, "Unknown Calling AET: " + callingAET);
            Attributes eventInfo = calculateStorageCommitmentResult(calledAET, actionInfo);
            try {
                as.writeDimseRSP(pc, rsp, null);
                device.execute(new SendStgCmtResult(as, eventInfo, stgCmtOnSameAssoc, remoteConnection));
            } catch (AssociationStateException e) {
                LOG.warn("{} << N-ACTION-RSP failed: {}", as, e.getMessage());
            }
        }

    }

    private final class CFindSCPImpl extends BasicCFindSCP {

        private final String[] qrLevels;
        private final QueryRetrieveLevel rootLevel;

        public CFindSCPImpl(String sopClass, String... qrLevels) {
            super(sopClass);
            this.qrLevels = qrLevels;
            this.rootLevel = QueryRetrieveLevel.valueOf(qrLevels[0]);
        }

        @Override
        protected QueryTask calculateMatches(Association as, PresentationContext pc, Attributes rq, Attributes keys)
                throws DicomServiceException {
            checkEmsowPermissions(as, "C-FIND");
            QueryRetrieveLevel level = QueryRetrieveLevel.valueOf(keys, qrLevels);
            level.validateQueryKeys(keys, rootLevel, rootLevel == QueryRetrieveLevel.IMAGE || relational(as, rq));
            IDicomReader ddr = getDicomDirReader();

            try {
                if (isUrl) {
                    ((DicomWebReader) ddr).sendWebRequest(UnvSCP.this.dicomUrl, UnvSCP.this.emsowUsername,
                            UnvSCP.this.emsowPassword, keys, IDicomReader.QRLevel.getEnum(level.name()),
                            UnvSCP.this, as);
                }
            } catch (UnknownHostException uhe) {
                throw new DicomServiceException(Status.UnableToProcess, "Unknown Host: " + uhe.getMessage());
            } catch (IOException ioe) {
                throw new DicomServiceException(Status.UnableToProcess, ioe.getMessage());
            }

            String availability = getInstanceAvailability();
            return new PatientQueryTask(as, pc, rq, keys, ddr, availability);
        }

        private boolean relational(Association as, Attributes rq) {
            String cuid = rq.getString(Tag.AffectedSOPClassUID);
            ExtendedNegotiation extNeg = as.getAAssociateAC().getExtNegotiationFor(cuid);
            return QueryOption.toOptions(extNeg).contains(QueryOption.RELATIONAL);
        }
    }

    private final class CGetSCPImpl extends BasicCGetSCP {

        private final String[] qrLevels;
        private final boolean withoutBulkData;
        private final QueryRetrieveLevel rootLevel;

        public CGetSCPImpl(String sopClass, String... qrLevels) {
            super(sopClass);
            this.qrLevels = qrLevels;
            this.withoutBulkData = qrLevels.length == 0;
            this.rootLevel = withoutBulkData ? QueryRetrieveLevel.IMAGE : QueryRetrieveLevel.valueOf(qrLevels[0]);
        }

        @Override
        protected RetrieveTask calculateMatches(Association as, PresentationContext pc, Attributes rq,
                Attributes keys) throws DicomServiceException {
            checkEmsowPermissions(as, "C-GET");
            QueryRetrieveLevel level = withoutBulkData ? QueryRetrieveLevel.IMAGE
                    : QueryRetrieveLevel.valueOf(keys, qrLevels);
            level.validateRetrieveKeys(keys, rootLevel, relational(as, rq));
            List<InstanceLocator> matches = UnvSCP.this.calculateMatches(keys, as, "C-GET");
            CGetRetrieveTask retrieveTask = new CGetRetrieveTask(as, pc, rq, matches, UnvSCP.this.dicomDownloadUrl,
                    UnvSCP.this.emsowUsername, UnvSCP.this.emsowPassword, UnvSCP.this, withoutBulkData);
            retrieveTask.setSendPendingRSP(isSendPendingCGet());
            return retrieveTask;
        }

        private boolean relational(Association as, Attributes rq) {
            String cuid = rq.getString(Tag.AffectedSOPClassUID);
            ExtendedNegotiation extNeg = as.getAAssociateAC().getExtNegotiationFor(cuid);
            return QueryOption.toOptions(extNeg).contains(QueryOption.RELATIONAL);
        }

    }

    private final class CMoveSCPImpl extends BasicCMoveSCP {

        private final String[] qrLevels;
        private final QueryRetrieveLevel rootLevel;

        public CMoveSCPImpl(String sopClass, String... qrLevels) {
            super(sopClass);
            this.qrLevels = qrLevels;
            this.rootLevel = QueryRetrieveLevel.valueOf(qrLevels[0]);
        }

        @Override
        protected RetrieveTask calculateMatches(Association as, PresentationContext pc, final Attributes rq,
                Attributes keys) throws DicomServiceException {
            checkEmsowPermissions(as, "C-MOVE");
            QueryRetrieveLevel level = QueryRetrieveLevel.valueOf(keys, qrLevels);
            level.validateRetrieveKeys(keys, rootLevel, relational(as, rq));
            String dest = rq.getString(Tag.MoveDestination);

            //If MoveDestination AET is not on the list then client's remote host and port 104 are used
            Connection cn = getRemoteConnection(dest);
            if (cn == null && !UnvSCP.this.allowUnknown) {
                throw new DicomServiceException(Status.MoveDestinationUnknown,
                        "Move destination is not configured for this AET: " + dest);
            } else {
                String defaultHost = (cn == null && UnvSCP.this.allowUnknown || cn.getHostname() == null
                        || "".equals(cn.getHostname().trim()) || "0.0.0.0".equals(cn.getHostname().trim()))
                                ? as.getSocket().getInetAddress().getHostAddress()
                                : cn.getHostname();

                int defaultPort = (cn == null && UnvSCP.this.allowUnknown || cn.getPort() == 0) ? 104
                        : cn.getPort();

                cn = UnvSCP.createAllowedConnection(defaultHost + ":" + defaultPort);
            }
            final Connection remote = cn;

            List<InstanceLocator> matches = UnvSCP.this.calculateMatches(keys, as, "C-MOVE");
            BasicRetrieveTask retrieveTask = new CMoveRetrieveTask(BasicRetrieveTask.Service.C_MOVE, as, pc, rq,
                    matches, UnvSCP.this.dicomDownloadUrl, UnvSCP.this.emsowUsername, UnvSCP.this.emsowPassword,
                    UnvSCP.this) {

                @Override
                protected Association getStoreAssociation() throws DicomServiceException {
                    try {
                        return as.getApplicationEntity().connect(as.getConnection(), remote, makeAAssociateRQ());
                    } catch (IOException e) {
                        LOG.info("{} << ERROR: {} {}:{}", new String[] { this.as.toString(), e.getMessage(),
                                remote.getHostname(), remote.getPort() + "" });
                        throw new DicomServiceException(Status.UnableToPerformSubOperations, e);
                    } catch (InterruptedException e) {
                        LOG.info("{} << ERROR: {} {}:{}", new String[] { this.as.toString(), e.getMessage(),
                                remote.getHostname(), remote.getPort() + "" });
                        throw new DicomServiceException(Status.UnableToPerformSubOperations, e);
                    } catch (IncompatibleConnectionException e) {
                        LOG.info("{} << ERROR: {} {}:{}", new String[] { this.as.toString(), e.getMessage(),
                                remote.getHostname(), remote.getPort() + "" });
                        throw new DicomServiceException(Status.UnableToPerformSubOperations, e);
                    } catch (GeneralSecurityException e) {
                        LOG.info("{} << ERROR: {} {}:{}", new String[] { this.as.toString(), e.getMessage(),
                                remote.getHostname(), remote.getPort() + "" });
                        throw new DicomServiceException(Status.UnableToPerformSubOperations, e);
                    }
                }

            };
            retrieveTask.setSendPendingRSPInterval(getSendPendingCMoveInterval());
            return retrieveTask;
        }

        private boolean relational(Association as, Attributes rq) {
            String cuid = rq.getString(Tag.AffectedSOPClassUID);
            ExtendedNegotiation extNeg = as.getAAssociateAC().getExtNegotiationFor(cuid);
            return QueryOption.toOptions(extNeg).contains(QueryOption.RELATIONAL);
        }
    }

    public UnvSCP(boolean isUrl) throws IOException {
        this.isUrl = isUrl;
        device.addConnection(conn);
        device.addApplicationEntity(ae);
        ae.setAssociationAcceptor(true);
        ae.addConnection(conn);
        device.setDimseRQHandler(createServiceRegistry());
    }

    public UnvSCP() throws IOException {
        this(false);
    }

    private void checkEmsowPermissions(Association as, String serviceCommand) throws DicomServiceException {
        if (serviceCommand == null) {
            throw new DicomServiceException(Status.UnableToProcess, "serviceCommand cannot be empty");
        }
        boolean isPush = serviceCommand.equalsIgnoreCase("C-STORE");
        boolean isPull = serviceCommand.matches("^((?i)C-GET|(?i)C-MOVE|(?i)C-FIND)$");

        String calledAet;
        String callingAet = as.getCallingAET();
        CAuthWebResponse.AERecord calledAeMeta, callingAeMeta;
        boolean isPullForUnknownEnabled, isPushForUnknownEnabled;
        synchronized (UnvSCP.this.emsowClientAets) {
            calledAeMeta = UnvSCP.this.emsowServerAeMeta;
            callingAeMeta = UnvSCP.this.emsowClientAets.get(callingAet);
            isPullForUnknownEnabled = UnvSCP.this.isPullForUnknownEnabled;
            isPushForUnknownEnabled = UnvSCP.this.isPushForUnknownEnabled;
        }

        if (calledAeMeta == null) { // impossible in normal case
            calledAet = UnvWebClient.getCalledAetOverride(as.getCalledAET());
            throw new DicomServiceException(Status.UnableToProcess,
                    "Server AET '" + calledAet + "' is missing in PACS Clients Module");
        }
        calledAet = calledAeMeta.getAET();
        if (isPush && !calledAeMeta.isPushEnabled()) {
            throw new DicomServiceException(Status.UnableToProcess,
                    "Pushing any data upon server with AET '" + calledAet + "' is not allowed");
        }
        if (isPull && !calledAeMeta.isPullEnabled()) {
            throw new DicomServiceException(Status.UnableToProcess,
                    "Pulling any data from server with AET '" + calledAet + "' is not allowed");
        }

        if (callingAeMeta == null) {
            if (!calledAeMeta.isUnknownAllowed()) {
                throw new DicomServiceException(Status.UnableToProcess,
                        "Client AET '" + callingAet + "' is missing in PACS Clients Module");
            }
            if (isPush && !isPushForUnknownEnabled) {
                throw new DicomServiceException(Status.UnableToProcess,
                        "Pushing any data from unknown clients is not allowed");
            }
            if (isPull && !isPullForUnknownEnabled) {
                throw new DicomServiceException(Status.UnableToProcess,
                        "Pulling any data onto unknown clients is not allowed");
            }
        } else {
            List<String> AllowedClients = calledAeMeta.getClients();
            boolean isClientAllowed = calledAeMeta.isUnknownAllowed() || AllowedClients == null
                    || AllowedClients.contains(callingAeMeta.getID());
            if (isPush && !(callingAeMeta.isPushEnabled() && isClientAllowed)) {
                throw new DicomServiceException(Status.UnableToProcess,
                        "Pushing any data from client with AET '" + callingAet + "' is not allowed");
            }
            if (isPull && !(callingAeMeta.isPullEnabled() && isClientAllowed)) {
                throw new DicomServiceException(Status.UnableToProcess,
                        "Pulling any data onto client with AET '" + callingAet + "' is not allowed");
            }
        }
    }

    private void storeTo(Association as, Attributes fmi, PDVInputStream data, File file, String iuid)
            throws IOException {
        LOG.info("{}: M-WRITE-BEGIN {}, UID={}", new Object[] { as, file, iuid });
        file.getParentFile().mkdirs();
        DicomOutputStream out = new DicomOutputStream(file);
        try {
            out.writeFileMetaInformation(fmi);
            data.copyTo(out);
        } finally {
            SafeClose.close(out);
        }
        LOG.info("{}: M-WRITE-END {}, UID={}", new Object[] { as, file, iuid });
    }

    private File getDestinationFile(Attributes attrs) {
        File file = new File(storageDir, filePathFormat.format(attrs));
        while (file.exists())
            file = new File(file.getParentFile(), TagUtils.toHexString(new Random().nextInt()));
        return file;
    }

    private static void renameTo(Association as, File from, File dest) throws IOException {
        LOG.info("{}: M-RENAME {}", new Object[] { as, from, dest });
        dest.getParentFile().mkdirs();
        if (!from.renameTo(dest))
            throw new IOException("Failed to rename " + from + " to " + dest);
    }

    private static Attributes parse(File file) throws IOException {
        DicomInputStream in = new DicomInputStream(file);
        try {
            in.setIncludeBulkData(IncludeBulkData.NO);
            return in.readDataset(-1, Tag.PixelData);
        } finally {
            SafeClose.close(in);
        }
    }

    private static void deleteFile(Association as, File file, String iuid) {
        if (file.delete())
            LOG.info("{}: M-DELETE {}, UID={}", new Object[] { as, file, iuid });
        else
            LOG.warn("{}: M-DELETE {} failed! UID={}", new Object[] { as, file, iuid });
    }

    private DicomServiceRegistry createServiceRegistry() {
        DicomServiceRegistry serviceRegistry = new DicomServiceRegistry();
        serviceRegistry.addDicomService(new BasicCEchoSCP());

        if (isUrl)
            serviceRegistry.addDicomService(new CStoreWebSCPImpl());
        else
            serviceRegistry.addDicomService(new CStoreSCPImpl());

        serviceRegistry.addDicomService(new StgCmtSCPImpl());
        serviceRegistry.addDicomService(
                new CFindSCPImpl(UID.PatientRootQueryRetrieveInformationModelFIND, PATIENT_ROOT_LEVELS));
        serviceRegistry.addDicomService(
                new CFindSCPImpl(UID.StudyRootQueryRetrieveInformationModelFIND, STUDY_ROOT_LEVELS));
        serviceRegistry.addDicomService(new CFindSCPImpl(
                UID.PatientStudyOnlyQueryRetrieveInformationModelFINDRetired, PATIENT_STUDY_ONLY_LEVELS));
        serviceRegistry
                .addDicomService(new CFindSCPImpl(UID.ModalityWorklistInformationModelFIND, PATIENT_ROOT_LEVELS));
        serviceRegistry.addDicomService(
                new CGetSCPImpl(UID.PatientRootQueryRetrieveInformationModelGET, PATIENT_ROOT_LEVELS));
        serviceRegistry
                .addDicomService(new CGetSCPImpl(UID.StudyRootQueryRetrieveInformationModelGET, STUDY_ROOT_LEVELS));
        serviceRegistry.addDicomService(new CGetSCPImpl(UID.PatientStudyOnlyQueryRetrieveInformationModelGETRetired,
                PATIENT_STUDY_ONLY_LEVELS));
        serviceRegistry.addDicomService(new CGetSCPImpl(UID.CompositeInstanceRetrieveWithoutBulkDataGET));
        serviceRegistry.addDicomService(
                new CMoveSCPImpl(UID.PatientRootQueryRetrieveInformationModelMOVE, PATIENT_ROOT_LEVELS));
        serviceRegistry.addDicomService(
                new CMoveSCPImpl(UID.StudyRootQueryRetrieveInformationModelMOVE, STUDY_ROOT_LEVELS));
        serviceRegistry.addDicomService(new CMoveSCPImpl(
                UID.PatientStudyOnlyQueryRetrieveInformationModelMOVERetired, PATIENT_STUDY_ONLY_LEVELS));
        return serviceRegistry;
    }

    public final Device getDevice() {
        return device;
    }

    public final void setDicomDirectory(File dicomDir) {
        File storageDir = dicomDir.getParentFile();
        if (storageDir.mkdirs())
            System.out.println("M-WRITE " + storageDir);
        this.storageDir = storageDir;
        this.dicomDir = dicomDir;
    }

    public final void setDicomDirectory(URL dicomUrl, String username, String password, File workDir, File tmpDir,
            Boolean notConcurrent) {
        this.storageDir = workDir;
        this.tmpDir = tmpDir;
        this.dicomUrl = dicomUrl;
        this.emsowUsername = username;
        this.emsowPassword = password;
        this.notConcurrent = notConcurrent;
    }

    public final File getStorageDirectory() {
        return storageDir;
    }

    public final AttributesFormat getFilePathFormat() {
        return filePathFormat;
    }

    public void setFilePathFormat(String pattern) {
        this.filePathFormat = new AttributesFormat(pattern);
    }

    public final File getDicomDirectory() {
        return dicomDir;
    }

    public final URL getDicomURL() {
        return dicomUrl;
    }

    public boolean isWriteable() {
        if (!isUrl) {
            return storageDir.canWrite();
        } else {
            return true;
        }
    }

    public final void setInstanceAvailability(String availability) {
        this.availability = availability;
    }

    public final String getInstanceAvailability() {
        return availability;
    }

    public boolean isStgCmtOnSameAssoc() {
        return stgCmtOnSameAssoc;
    }

    public void setStgCmtOnSameAssoc(boolean stgCmtOnSameAssoc) {
        this.stgCmtOnSameAssoc = stgCmtOnSameAssoc;
    }

    public final void setSendPendingCGet(boolean sendPendingCGet) {
        this.sendPendingCGet = sendPendingCGet;
    }

    public final boolean isSendPendingCGet() {
        return sendPendingCGet;
    }

    public final void setSendPendingCMoveInterval(int sendPendingCMoveInterval) {
        this.sendPendingCMoveInterval = sendPendingCMoveInterval;
    }

    public final int getSendPendingCMoveInterval() {
        return sendPendingCMoveInterval;
    }

    public final void setRecordFactory(RecordFactory recFact) {
        this.recFact = recFact;
    }

    public final RecordFactory getRecordFactory() {
        return recFact;
    }

    private static CommandLine parseComandLine(String[] args) throws ParseException {
        Options opts = new Options();
        CLIUtils.addFilesetInfoOptions(opts);
        CLIUtils.addBindServerOption(opts);
        CLIUtils.addConnectTimeoutOption(opts);
        CLIUtils.addAcceptTimeoutOption(opts);
        CLIUtils.addAEOptions(opts);
        CLIUtils.addCommonOptions(opts);
        CLIUtils.addResponseTimeoutOption(opts);
        addDicomDirOption(opts);
        addTransferCapabilityOptions(opts);
        addInstanceAvailabilityOption(opts);
        addStgCmtOptions(opts);
        addSendingPendingOptions(opts);
        addRemoteConnectionsOption(opts);
        addLogOptions(opts);
        addAsyncOptions(opts);
        addUploadDirOptions(opts);
        addCompressionOptions(opts);
        addPushOptions(opts);
        addBridgeOptions(opts);
        addSessionOptions(opts);
        return CLIUtils.parseComandLine(args, opts, rb, UnvSCP.class);
    }

    @SuppressWarnings("static-access")
    private static void addLogOptions(Options opts) {
        opts.addOption(OptionBuilder.hasArg().withArgName("log-dir").withDescription(rb.getString("log-dir"))
                .withLongOpt("log-dir").create());
        opts.addOption(OptionBuilder.hasArg().withArgName("store-log").withDescription(rb.getString("store-log"))
                .withLongOpt("store-log").create());
    }

    @SuppressWarnings("static-access")
    private static void addAsyncOptions(Options opts) {
        opts.addOption(null, "async", false, rb.getString("asynchronous-mode"));
        opts.addOption(OptionBuilder.hasArg().withArgName("bad-files-dir")
                .withDescription(rb.getString("bad-files-dir")).withLongOpt("bad-files-dir").create());
        opts.addOption(
                OptionBuilder.hasArg().withArgName("interval").withDescription(rb.getString("async-sleep-interval"))
                        .withLongOpt("async-sleep-interval").create());
    }

    @SuppressWarnings("static-access")
    private static void addUploadDirOptions(Options opts) {
        opts.addOption(OptionBuilder.hasArg().withArgName("upl-dir").withDescription(rb.getString("upl-dir"))
                .withLongOpt("upl-dir").create());
        opts.addOption(OptionBuilder.hasArg().withArgName("upl-bad-files-dir")
                .withDescription(rb.getString("upl-bad-files-dir")).withLongOpt("upl-bad-files-dir").create());
        opts.addOption(OptionBuilder.hasArg().withArgName("upl-sleep-interval")
                .withDescription(rb.getString("upl-sleep-interval")).withLongOpt("upl-sleep-interval").create());
        opts.addOption(OptionBuilder.hasArg().withArgName("[aet[@ip]:]port")
                .withDescription(rb.getString("source-override")).withLongOpt("source-override").create());
        opts.addOption(OptionBuilder.hasArg().withArgName("days amount")
                .withDescription(rb.getString("store-upl-failures")).withLongOpt("store-upl-failures").create());
    }

    @SuppressWarnings("static-access")
    private static void addCompressionOptions(Options opts) {
        opts.addOption(OptionBuilder.hasArg().withArgName("[0..9]").withDescription(rb.getString("compression"))
                .withLongOpt("compression").create());
    }

    @SuppressWarnings("static-access")
    private static void addPushOptions(Options opts) {
        opts.addOption(OptionBuilder.hasArg().withArgName("PUT|POST")
                .withDescription(rb.getString("push-http-method")).withLongOpt("push-http-method").create());
    }

    @SuppressWarnings("static-access")
    private static void addInstanceAvailabilityOption(Options opts) {
        opts.addOption(OptionBuilder.hasArg().withArgName("code").withDescription(rb.getString("availability"))
                .withLongOpt("availability").create());
    }

    @SuppressWarnings("static-access")
    private static void addBridgeOptions(Options opts) {
        opts.addOption(OptionBuilder.hasArg().withArgName("[aet[@ip]]")
                .withDescription(rb.getString("destination-override")).withLongOpt("destination-override")
                .create());
        opts.addOption(null, "gui", false, rb.getString("visual-interface"));
    }

    @SuppressWarnings("static-access")
    private static void addSessionOptions(Options opts) {
        opts.addOption(OptionBuilder.hasArg().withArgName("str").withDescription(rb.getString("emsow-session-name"))
                .withLongOpt("emsow-session-name").create());
    }

    private static void addStgCmtOptions(Options opts) {
        opts.addOption(null, "stgcmt-same-assoc", false, rb.getString("stgcmt-same-assoc"));
    }

    @SuppressWarnings("static-access")
    private static void addSendingPendingOptions(Options opts) {
        opts.addOption(null, "pending-cget", false, rb.getString("pending-cget"));
        opts.addOption(OptionBuilder.hasArg().withArgName("s").withDescription(rb.getString("pending-cmove"))
                .withLongOpt("pending-cmove").create());
    }

    @SuppressWarnings("static-access")
    private static void addDicomDirOption(Options opts) {
        opts.addOption(OptionBuilder.hasArg().withArgName("file").withDescription(rb.getString("dicomdir"))
                .withLongOpt("dicomdir").create());
        opts.addOption(OptionBuilder.hasArg().withArgName("pattern").withDescription(rb.getString("filepath"))
                .withLongOpt("filepath").create(null));
        opts.addOption(OptionBuilder.hasArg().withArgName("emsow-url").withDescription(rb.getString("emsow-url"))
                .withLongOpt("emsow-url").create());
        opts.addOption(OptionBuilder.hasArg().withArgName("emsow-username")
                .withDescription(rb.getString("emsow-username")).withLongOpt("emsow-username").create());
        opts.addOption(OptionBuilder.hasArg().withArgName("emsow-password")
                .withDescription(rb.getString("emsow-password")).withLongOpt("emsow-password").create());
        opts.addOption(OptionBuilder.hasArg().withArgName("work-dir").withDescription(rb.getString("work-dir"))
                .withLongOpt("work-dir").create());
        opts.addOption(OptionBuilder.hasArg().withArgName("file").withDescription(rb.getString("tmp-dir"))
                .withLongOpt("tmp-dir").create());
        opts.addOption(null, "not-concurrent", false, rb.getString("not-concurrent"));
    }

    @SuppressWarnings("static-access")
    private static void addTransferCapabilityOptions(Options opts) {
        opts.addOption(null, "all-storage", false, rb.getString("all-storage"));
        opts.addOption(null, "no-storage", false, rb.getString("no-storage"));
        opts.addOption(null, "no-query", false, rb.getString("no-query"));
        opts.addOption(null, "no-retrieve", false, rb.getString("no-retrieve"));
        opts.addOption(null, "relational", false, rb.getString("relational"));
        opts.addOption(null, "allow-unknown", false, rb.getString("allow-unknown"));
        opts.addOption(OptionBuilder.hasArg().withArgName("file|url")
                .withDescription(rb.getString("storage-sop-classes")).withLongOpt("storage-sop-classes").create());
        opts.addOption(OptionBuilder.hasArg().withArgName("file|url")
                .withDescription(rb.getString("query-sop-classes")).withLongOpt("query-sop-classes").create());
        opts.addOption(
                OptionBuilder.hasArg().withArgName("file|url").withDescription(rb.getString("retrieve-sop-classes"))
                        .withLongOpt("retrieve-sop-classes").create());
    }

    @SuppressWarnings("static-access")
    private static void addRemoteConnectionsOption(Options opts) {
        opts.addOption(OptionBuilder.hasArg().withArgName("file|url").withDescription(rb.getString("ae-config"))
                .withLongOpt("ae-config").create());
    }

    public static void main(String[] args) {
        try {
            CommandLine cl = parseComandLine(args);
            final UnvSCP main = new UnvSCP(!cl.hasOption("dicomdir"));
            CLIUtils.configure(main.fsInfo, cl);
            CLIUtils.configureBindServer(main.conn, main.ae, cl);
            CLIUtils.configure(main.conn, cl);
            configureDicomFileSet(main, cl);
            configureLog(main, cl);
            configureAsyncMode(main, cl);
            configureManualUploading(main, cl);
            configureCompression(main, cl);
            configurePushMethod(main, cl);
            configureTransferCapability(main, cl);
            configureRoleSelectionsAndExtendedNegotiations(main, cl);
            configureInstanceAvailability(main, cl);
            configureStgCmt(main, cl);
            configureSendPending(main, cl);
            configureDestinationOverride(main, cl);
            configureSession(main, cl);
            configureRemoteConnections(main, cl);
            configureVisualInterface(main, cl);
            ExecutorService executorService = Executors.newCachedThreadPool();
            ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
            main.device.setScheduledExecutor(scheduledExecutorService);
            main.device.setExecutor(executorService);
            main.device.bindConnections();

            main.device.setAssociationHandler(new AssociationHandler() {
                @Override
                protected AAssociateAC negotiate(Association as, AAssociateRQ rq) throws IOException {
                    //as.getCallingAET(); as.getRemoteAET(); // show the same result

                    LOG.info("{} >> A-ASSOCIATE-RQ {}",
                            "Association" + as.toString().replaceFirst("\\b[^+^-]*", ""),
                            (rq != null ? rq.getCallingAET() : ""));

                    try {
                        UnvWebClient webClient = new UnvWebClient(main.dicomUrl.toString());
                        webClient.addListener(main, new DicomClientMetaInfo(as));
                        webClient.setBasicParams(main.emsowUsername, main.emsowPassword, "EMSOW-AUTH", as);

                        if (main.notConcurrent) {
                            synchronized (main) {
                                webClient.sendPostRequest();
                            }
                        } else {
                            webClient.sendPostRequest();
                        }

                        CAuthWebResponse cawr = webClient.parseJsonResponse(CAuthWebResponse.class);
                        Collection<CAuthWebResponse.AERecord> ae_client_list = cawr.getClientsData();
                        if (ae_client_list != null) {
                            synchronized (main.emsowClientAets) {
                                main.emsowServerAeMeta = cawr.getServerData();
                                main.emsowClientAets.clear();
                                for (CAuthWebResponse.AERecord aerec : ae_client_list) {
                                    main.emsowClientAets.put(aerec.getAET(), aerec);
                                }
                                main.isPullForUnknownEnabled = cawr.isPullForUnknownEnabled();
                                main.isPushForUnknownEnabled = cawr.isPushForUnknownEnabled();
                            }
                            if (UnvSCP.isDynamicAEList) {
                                synchronized (main.remoteConnections) {
                                    main.remoteConnections.clear();
                                    for (CAuthWebResponse.AERecord aerec : ae_client_list) {
                                        String host = (aerec.getHost() == null) ? "" : aerec.getHost();
                                        String port = (aerec.getHost() == null || "".equals(aerec.getPort().trim()))
                                                ? "104"
                                                : aerec.getPort();
                                        main.addRemoteConnection(aerec.getAET(),
                                                UnvSCP.createAllowedConnection(host + ":" + port));
                                    }
                                }
                            }
                        }
                    } catch (IOException ioe) {
                        throw new UnvAAssociateRJ(UnvAAssociateRJ.RESULT_REJECTED_PERMANENT,
                                UnvAAssociateRJ.SOURCE_SERVICE_USER,
                                UnvAAssociateRJ.REASON_CALLING_AET_NOT_RECOGNIZED, ioe.getMessage());
                    } catch (Exception e) {
                        throw new UnvAAssociateRJ(UnvAAssociateRJ.RESULT_REJECTED_PERMANENT,
                                UnvAAssociateRJ.SOURCE_SERVICE_USER, UnvAAssociateRJ.REASON_USER_DEFINED,
                                e.getMessage());
                    }

                    AAssociateAC ac = super.negotiate(as, rq);
                    for (RoleSelection rs : main.roleSelections) {
                        ac.addRoleSelection(rs);
                    }
                    for (ExtendedNegotiation extNeg : main.extendedNegotiaions) {
                        ac.addExtendedNegotiation(extNeg);
                    }
                    return ac;
                }
            });

            if (main.async) {
                AsyncSenderTask asyncSenderTask = new AsyncSenderTask(main.dicomUrl, main.emsowUsername,
                        main.emsowPassword, main.storageDir, main.tmpDir, main.badFilesDir, main.sleepTime,
                        main.notConcurrent, main.compressionLevel, main);
                asyncSenderTask.addSenderTaskListener(main.activityWindow);
                main.asyncSender = new Thread(asyncSenderTask, "ASYNC");
                main.asyncSender.setDaemon(true);
                main.asyncSender.start();
            }

            if (main.uplDir != null && main.uplBadFilesDir != null) {
                Properties params = UnvWebClient.getUploadParams(main.ae, main.conn);
                DcmFileSenderTask dcmFileSenderTask = new DcmFileSenderTask(main.dicomUrl, main.emsowUsername,
                        main.emsowPassword, main.uplDir, main.tmpDir, main.uplBadFilesDir, main.uplSleepTime,
                        main.notConcurrent, main.compressionLevel, main, params);
                dcmFileSenderTask.addSenderTaskListener(main.activityWindow);
                main.dcmFileSender = new Thread(dcmFileSenderTask, "UPL");
                main.dcmFileSender.setDaemon(true);
                main.dcmFileSender.start();
            }
        } catch (ParseException e) {
            System.err.println("unvscp: " + e.getMessage());
            System.err.println(rb.getString("try"));
            System.exit(2);
        } catch (Exception e) {
            System.err.println("unvscp: " + e.getMessage());
            e.printStackTrace();
            System.exit(2);
        }
    }

    private static void configureAsyncMode(UnvSCP main, CommandLine cl) throws ParseException {
        if (cl.hasOption("async")) {
            if (!cl.hasOption("bad-files-dir")) {
                throw new MissingOptionException(rb.getString("missing-bad-files-dir"));
            }

            File badFilesDir = new File(cl.getOptionValue("bad-files-dir"));
            if (!badFilesDir.exists()) {
                throw new ParseException("bad-files-dir '" + badFilesDir.getPath()
                        + "' doesn't exist. Create the directory or change option --bad-files-dir");
            }
            if (!badFilesDir.isDirectory()) {
                throw new ParseException("bad-files-dir '" + badFilesDir.getPath()
                        + "' is not a directory. Change option --bad-files-dir");
            }
            if (!badFilesDir.canRead()) {
                throw new ParseException("Can not read from bad-files-dir '" + badFilesDir.getPath()
                        + "'. Check access permissions or change option --bad-files-dir");
            }
            if (!badFilesDir.canWrite()) {
                throw new ParseException("Can not write to bad-files-dir '" + badFilesDir.getPath()
                        + "'. Check access permissions or change option --bad-files-dir");
            }

            main.async = true;
            main.badFilesDir = badFilesDir;
            try {
                main.sleepTime = Integer.parseInt(cl.getOptionValue("async-sleep-interval", "5"));
            } catch (NumberFormatException nfe) {
                throw new ParseException("\"" + cl.getOptionValue("async-sleep-interval")
                        + "\" is not a valid integer value for --async-sleep-interval");
            }
        }
    }

    private static void configureManualUploading(UnvSCP main, CommandLine cl) throws ParseException {
        if (cl.hasOption("upl-dir")) {
            if (!cl.hasOption("upl-bad-files-dir")) {
                throw new MissingOptionException(rb.getString("missing-upl-bad-files"));
            }

            File uplDir = new File(cl.getOptionValue("upl-dir"));
            if (!uplDir.exists()) {
                throw new ParseException("upl-dir '" + uplDir.getPath()
                        + "' doesn't exist. Create the directory or change option --upl-dir");
            }
            if (!uplDir.isDirectory()) {
                throw new ParseException(
                        "upl-dir '" + uplDir.getPath() + "' is not a directory. Change option --upl-dir");
            }
            if (!uplDir.canRead()) {
                throw new ParseException("Can not read from upl-dir '" + uplDir.getPath()
                        + "'. Check access permissions or change option --upl-dir");
            }
            if (!uplDir.canWrite()) {
                throw new ParseException("Can not write to upl-dir '" + uplDir.getPath()
                        + "'. Check access permissions or change option --upl-dir");
            }

            File uplBadFilesDir = new File(cl.getOptionValue("upl-bad-files-dir"));
            if (!uplBadFilesDir.exists()) {
                throw new ParseException("upl-bad-files-dir '" + uplBadFilesDir.getPath()
                        + "' doesn't exist. Create the directory or change option --upl-bad-files-dir");
            }
            if (!uplBadFilesDir.isDirectory()) {
                throw new ParseException("upl-bad-files-dir '" + uplBadFilesDir.getPath()
                        + "' is not a directory. Change option --upl-bad-files-dir");
            }
            if (!uplBadFilesDir.canRead()) {
                throw new ParseException("Can not read from upl-bad-files-dir '" + uplBadFilesDir.getPath()
                        + "'. Check access permissions or change option --upl-bad-files-dir");
            }
            if (!uplBadFilesDir.canWrite()) {
                throw new ParseException("Can not write to upl-bad-files-dir '" + uplBadFilesDir.getPath()
                        + "'. Check access permissions or change option --upl-bad-files-dir");
            }

            main.uplDir = uplDir;
            main.uplBadFilesDir = uplBadFilesDir;
            try {
                main.uplSleepTime = Integer.parseInt(cl.getOptionValue("upl-sleep-interval", "5"));
            } catch (NumberFormatException nfe) {
                throw new ParseException("\"" + cl.getOptionValue("upl-sleep-interval")
                        + "\" is not a valid integer value for --upl-sleep-interval");
            }

            if (cl.hasOption("source-override")) {
                UnvWebClient.setSourceOverride(cl.getOptionValue("source-override"));
            }

            if (cl.hasOption("store-upl-failures")) {
                final Integer days;
                try {
                    days = Integer.parseInt(cl.getOptionValue("store-upl-failures"));
                } catch (NumberFormatException nfe) {
                    throw new ParseException("--store-upl-failures can not accept \""
                            + cl.getOptionValue("store-upl-failures") + "\" (only integer values are allowed)");
                }

                FileFilter filter = new FileFilter() {
                    private Date theDate = new Date();
                    {
                        Calendar c = Calendar.getInstance();
                        c.setTime(theDate);
                        c.add(Calendar.DATE, -1 * Math.abs(days));
                        theDate = c.getTime();
                    }

                    @Override
                    public boolean accept(File file) {
                        return file.isDirectory() || file.lastModified() < this.theDate.getTime();
                    }
                };

                doRecursiveCleanUp(uplBadFilesDir, true, filter);
            }
        }
    }

    public static void doRecursiveCleanUp(File dir, boolean saveRoot, FileFilter filter) {
        File[] children = dir.listFiles(filter);
        for (File child : children) {
            if (child.isDirectory()) {
                doRecursiveCleanUp(child, false, filter);
            } else {
                if (child.delete()) {
                    LOG.info("Old file {} has been removed", child.getName());
                } else {
                    LOG.warn(
                            "Can not delete file {}. The file may be in use or the system settings do not allow this operation",
                            child.getName());
                }
            }
        }
        if (!saveRoot) {
            dir.delete();
        }
    }

    private static void configureCompression(UnvSCP main, CommandLine cl) throws ParseException {
        if (cl.hasOption("compression")) {
            try {
                Integer level = Integer.parseInt(cl.getOptionValue("compression"));
                main.compressionLevel = (level == 0 ? null : level);
            } catch (NumberFormatException nfe) {
                throw new ParseException("--compression can not accept \"" + cl.getOptionValue("compression")
                        + "\" (only integer values are allowed)");
            }
        }
    }

    private static void configurePushMethod(UnvSCP main, CommandLine cl) throws ParseException {
        String method = null;
        if (cl.hasOption("push-http-method") && !"".equals(method = cl.getOptionValue("push-http-method"))) {
            if (HttpPost.METHOD_NAME.equalsIgnoreCase(method)) {
                UnvWebClient.setUploadingFilesHttpMethod(HttpPost.METHOD_NAME);
            } else if (HttpPut.METHOD_NAME.equalsIgnoreCase(method)) {
                UnvWebClient.setUploadingFilesHttpMethod(HttpPut.METHOD_NAME);
            } else {
                throw new ParseException(
                        "--push-http-method accepts only POST|PUT. Method \"" + method + "\" is not supported.");
            }
        } else {
            UnvWebClient.setUploadingFilesHttpMethod(HttpPut.METHOD_NAME);
        }

        LOG.info("Using http method \"{}\" for uploading files", UnvWebClient.getUploadingFilesHttpMethod());
    }

    private static void configureLog(UnvSCP main, CommandLine cl)
            throws ParseException, FileNotFoundException, IOException {
        File logDir = null;
        if (cl.hasOption("log-dir")) {
            logDir = new File(cl.getOptionValue("log-dir"));
            if (!logDir.exists()) {
                throw new ParseException("log-dir '" + logDir.getPath()
                        + "' doesn't exist. Create the directory or change option --log-dir");
            }
            if (!logDir.isDirectory()) {
                throw new ParseException(
                        "log-dir '" + logDir.getPath() + "' is not a directory. Change option --log-dir");
            }
            if (!logDir.canRead()) {
                throw new ParseException("Can not read from log-dir '" + logDir.getPath()
                        + "'. Check access permissions or change option --log-dir");
            }
            if (!logDir.canWrite()) {
                throw new ParseException("Can not write to log-dir '" + logDir.getPath()
                        + "'. Check access permissions or change option --log-dir");
            }

            Properties log4jProps = new Properties();
            InputStream configStream = main.getClass().getResourceAsStream("/log4j.properties");
            log4jProps.load(configStream);
            configStream.close();

            log4jProps.setProperty("log4j.rootLogger", "INFO, stdout, file");
            //log4jProps.setProperty("log4j.appender.file", "org.apache.log4j.DailyRollingFileAppender");
            log4jProps.setProperty("log4j.appender.file", "org.apache.log4j.rolling.RollingFileAppender");
            log4jProps.setProperty("log4j.appender.file.File", new File(logDir, "latest.log").getPath());
            log4jProps.setProperty("log4j.appender.file.ImmediateFlush", "true");
            log4jProps.setProperty("log4j.appender.file.Append", "true");
            //log4jProps.setProperty("log4j.appender.file.DatePattern", "'-'yyyy-MM-dd-mm-ss'.log'");
            //log4jProps.setProperty("log4j.appender.file.MaxBackupIndex", "7");
            //log4jProps.setProperty("log4j.appender.file.triggeringPolicy", "org.apache.log4j.rolling.TimeBasedRollingPolicy");
            //log4jProps.setProperty("log4j.appender.file.triggeringPolicy.FileNamePattern", new File(logDir, "%d{yyyy-MM-dd-mm-ss}.log").getPath());
            log4jProps.setProperty("log4j.appender.file.RollingPolicy",
                    "org.apache.log4j.rolling.TimeBasedRollingPolicy");
            log4jProps.setProperty("log4j.appender.file.RollingPolicy.FileNamePattern",
                    new File(logDir, "%d{yyyy-MM-dd}.log").getPath());
            log4jProps.setProperty("log4j.appender.file.layout", "org.apache.log4j.PatternLayout");
            log4jProps.setProperty("log4j.appender.file.layout.ConversionPattern",
                    "%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n");

            LogManager.resetConfiguration();
            PropertyConfigurator.configure(log4jProps);

        } else {
            if (cl.hasOption("store-log")) {
                throw new MissingOptionException(rb.getString("missing-log-dir"));
            }
        }

        if (cl.hasOption("store-log")) {
            final Integer days;
            try {
                days = Integer.parseInt(cl.getOptionValue("store-log"));
            } catch (NumberFormatException nfe) {
                throw new ParseException("--store-log can not accept \"" + cl.getOptionValue("store-log")
                        + "\" (only integer values are allowed)");
            }

            // Temporary Solution for Log clean up
            FileFilter filter = new FileFilter() {
                private Date theDate = new Date();
                {
                    Calendar c = Calendar.getInstance();
                    c.setTime(theDate);
                    c.add(Calendar.DATE, -1 * Math.abs(days));
                    theDate = c.getTime();
                }

                @Override
                public boolean accept(File file) {
                    return file.isFile() && file.lastModified() < this.theDate.getTime()
                            && file.getName().matches("^\\d\\d\\d\\d-\\d\\d-\\d\\d\\.(?i)(log)$");
                }
            };

            File[] logFilesToRemove = logDir.listFiles(filter);
            if (logFilesToRemove.length > 0) {
                LOG.info("Cleaning up the log dir (There are {} old log files to remove)", logFilesToRemove.length);
            }
            for (File f : logFilesToRemove) {
                if (!f.delete()) {
                    LOG.warn(
                            "Can not delete log file {}. The file may be in use or the system settings do not allow this operation",
                            f.getName());
                }
            }
        }
    }

    private static void configureDicomFileSet(UnvSCP main, CommandLine cl)
            throws ParseException, MalformedURLException, IOException {
        if (!cl.hasOption("dicomdir") && !cl.hasOption("emsow-url")) {
            throw new MissingOptionException(rb.getString("missing-dicomdir"));
        }
        if (cl.hasOption("dicomdir")) {
            main.isUrl = false;
            main.setDicomDirectory(new File(cl.getOptionValue("dicomdir")));
        } else { //if emsow-url is specified
            if (!cl.hasOption("emsow-username") || !cl.hasOption("emsow-password") || !cl.hasOption("work-dir")
                    || !cl.hasOption("tmp-dir")) {
                throw new MissingOptionException(rb.getString("missing-emsow"));
            }
            main.isUrl = true;
            main.setDicomDirectory(new URL(cl.getOptionValue("emsow-url")), cl.getOptionValue("emsow-username"),
                    cl.getOptionValue("emsow-password"), new File(cl.getOptionValue("work-dir")),
                    new File(cl.getOptionValue("tmp-dir")), cl.hasOption("not-concurrent"));
        }
        main.setFilePathFormat(
                cl.getOptionValue("filepath", "DICOM/{0020000D,hash}/{0020000E,hash}/{00080018,hash}"));
        main.setRecordFactory(new RecordFactory());

        main.allowUnknown = cl.hasOption("allow-unknown");
    }

    private static void configureInstanceAvailability(UnvSCP main, CommandLine cl) {
        main.setInstanceAvailability(cl.getOptionValue("availability"));
    }

    private static void configureStgCmt(UnvSCP main, CommandLine cl) {
        main.setStgCmtOnSameAssoc(cl.hasOption("stgcmt-same-assoc"));
    }

    private static void configureSendPending(UnvSCP main, CommandLine cl) {
        main.setSendPendingCGet(cl.hasOption("pending-cget"));
        if (cl.hasOption("pending-cmove"))
            main.setSendPendingCMoveInterval(Integer.parseInt(cl.getOptionValue("pending-cmove")));
    }

    private static void configureTransferCapability(UnvSCP main, CommandLine cl) throws IOException {
        ApplicationEntity ae = main.ae;
        EnumSet<QueryOption> queryOptions = cl.hasOption("relational") ? EnumSet.of(QueryOption.RELATIONAL)
                : EnumSet.noneOf(QueryOption.class);
        boolean storage = !cl.hasOption("no-storage") && main.isWriteable();
        if (storage && cl.hasOption("all-storage")) {
            TransferCapability tc = new TransferCapability(null, "*", TransferCapability.Role.SCP, "*");
            tc.setQueryOptions(queryOptions);
            ae.addTransferCapability(tc);
        } else {
            ae.addTransferCapability(new TransferCapability(null, UID.VerificationSOPClass,
                    TransferCapability.Role.SCP, UID.ImplicitVRLittleEndian));
            Properties storageSOPClasses = CLIUtils.loadProperties(
                    cl.getOptionValue("storage-sop-classes", "resource:storage-sop-classes.properties"), null);
            if (storage)
                addTransferCapabilities(ae, storageSOPClasses, TransferCapability.Role.SCP, null);
            if (!cl.hasOption("no-retrieve")) {
                addTransferCapabilities(ae, storageSOPClasses, TransferCapability.Role.SCU, null);
                Properties p = CLIUtils.loadProperties(
                        cl.getOptionValue("retrieve-sop-classes", "resource:retrieve-sop-classes.properties"),
                        null);
                addTransferCapabilities(ae, p, TransferCapability.Role.SCP, queryOptions);
            }
            if (!cl.hasOption("no-query")) {
                Properties p = CLIUtils.loadProperties(
                        cl.getOptionValue("query-sop-classes", "resource:query-sop-classes.properties"), null);
                addTransferCapabilities(ae, p, TransferCapability.Role.SCP, queryOptions);
            }
        }
        if (storage && !main.isUrl)
            main.openDicomDir();
        else
            main.openDicomDirForReadOnly();
    }

    /**
     * Since we store the meta data in the db we should use the relational (not hierarchical) model
     * to check for missing attributes in the association request
     * for retrieving images by C-GET/C-MOVE or querying by C-FIND.<br>
     * Key "<b>--relational</b>" now has effect. Always use it when launching UnvSCP. <br><br>
     * Config file <b>storage-sop-classes.properties</b> is now also used for creating role selections
     * that enable to transfer specific sop classes via C-GET
     */
    private static void configureRoleSelectionsAndExtendedNegotiations(UnvSCP main, CommandLine cl) {
        for (TransferCapability tc : main.ae.getTransferCapabilitiesWithRole(TransferCapability.Role.SCP)) {
            EnumSet<QueryOption> queryOpts = tc.getQueryOptions();
            /**
             * if queryOpts == null then we are dealing with storage sop classes for which we have to create role selections,
             * otherwise it's about query or retrieve sop classes for which we have to create extended negotiations containing the queryOpts
             */
            if (queryOpts == null) {
                main.roleSelections.add(new RoleSelection(tc.getSopClass(), true, true));
            } else {
                if (!queryOpts.isEmpty()) {
                    main.extendedNegotiaions.add(new ExtendedNegotiation(tc.getSopClass(),
                            QueryOption.toExtendedNegotiationInformation(queryOpts)));
                }
            }
        }
    }

    private static void configureDestinationOverride(UnvSCP main, CommandLine cl) throws ParseException {

        if (cl.hasOption("destination-override")) {
            /* delete?
            if (cl.getOptionValue("destination-override") == null) {
            throw new ParseException("--destination-override cannot be empty");
            }
            */

            if (!cl.hasOption("log-dir") || !cl.hasOption("store-log")) {
                throw new MissingOptionException(rb.getString("missing-bridge"));
            }
            UnvWebClient.setDestinationOverride(cl.getOptionValue("destination-override"));
        }
    }

    private static void configureSession(UnvSCP main, CommandLine cl) {
        setEmsowSessionName(cl);
    }

    public static void setEmsowSessionName(CommandLine cl) {
        if (!cl.hasOption("emsow-session-name") || "".equals(cl.getOptionValue("emsow-session-name").trim())) {
            UnvWebClient.setSessionName("bridge");
        } else {
            UnvWebClient.setSessionName(cl.getOptionValue("emsow-session-name"));
        }
    }

    private static void configureVisualInterface(UnvSCP main, CommandLine cl) {
        if (cl.hasOption("gui")) {
            main.activityWindow = ActivityWindow.launch();
        }
    }

    private static void addTransferCapabilities(ApplicationEntity ae, Properties p, TransferCapability.Role role,
            EnumSet<QueryOption> queryOptions) {
        for (String cuid : p.stringPropertyNames()) {
            String ts = p.getProperty(cuid);
            TransferCapability tc = new TransferCapability(null, cuid, role,
                    ts.equals("*") ? new String[] { "*" } : toUIDs(StringUtils.split(ts, ',')));
            tc.setQueryOptions(queryOptions);
            ae.addTransferCapability(tc);
        }
    }

    private static String[] toUIDs(String[] names) {
        String[] uids = new String[names.length];
        for (int i = 0; i < uids.length; i++)
            uids[i] = UID.forName(names[i].trim());
        return uids;
    }

    private static void configureRemoteConnections(UnvSCP main, CommandLine cl) throws Exception {
        String file = cl.getOptionValue("ae-config");
        if (file == null) {
            file = "resource:ae.properties";
            UnvSCP.isDynamicAEList = true;
        }
        Properties aeConfig = CLIUtils.loadProperties(file, null);
        for (Map.Entry<Object, Object> entry : aeConfig.entrySet()) {
            String aet = (String) entry.getKey();
            String value = (String) entry.getValue();
            try {
                main.addRemoteConnection(aet, UnvSCP.createAllowedConnection(value));
            } catch (Exception e) {
                throw new IllegalArgumentException("Invalid entry in " + file + ": " + aet + "=" + value);
            }
        }
    }

    /**
     *
     * @param value a String in the following Format: hostname:port[:cipher1[:...]]
     * <p><b>An example:</b><br>localhost:2762[:SSL_RSA_WITH_NULL_SHA[:TLS_RSA_WITH_AES_128_CBC_SHA[:TLS_RSA_WITH_3DES_EDE_CBC_SHA]]]</p>
     * @return a new Connection that should be later included in the list of allowed connections for C-MOVE operation
     */
    private static Connection createAllowedConnection(String value) {
        String[] hostPortCiphers = StringUtils.split(value, ':');
        String[] ciphers = new String[hostPortCiphers.length - 2];
        System.arraycopy(hostPortCiphers, 2, ciphers, 0, ciphers.length);
        Connection remote = new Connection();
        remote.setHostname(hostPortCiphers[0]);
        remote.setPort(Integer.parseInt(hostPortCiphers[1]));
        remote.setTlsCipherSuites(ciphers);
        return remote;
    }

    final IDicomReader getDicomDirReader() {
        return ddReader;
    }

    final DicomDirWriter getDicomDirWriter() {
        return ddWriter.getDicomDirWriter();
    }

    private void openDicomDir() throws IOException {
        if (!dicomDir.exists())
            DicomDirWriter.createEmptyDirectory(dicomDir, UIDUtils.createUIDIfNull(fsInfo.getFilesetUID()),
                    fsInfo.getFilesetID(), fsInfo.getDescriptorFile(), fsInfo.getDescriptorFileCharset());
        ddReader = ddWriter = DicomDirWriterWrapper.open(dicomDir);
    }

    private void openDicomDirForReadOnly() throws IOException {
        if (isUrl) {
            ddReader = new DicomWebReader(dicomUrl);
        } else {
            ddReader = new DicomDirReaderWrapper(dicomDir);
        }
    }

    public void addRemoteConnection(String aet, Connection remote) {
        remoteConnections.put(aet, remote);
    }

    Connection getRemoteConnection(String dest) {
        return remoteConnections.get(dest);
    }

    @Override
    public synchronized void logMessage(DicomClientMetaInfo metaInfo, String command, Object message,
            String dialog) {
        Association as = metaInfo.getAssoc();
        Properties extraData = metaInfo.getExtraData();
        String threadName = !"main".equals(Thread.currentThread().getName())
                ? " [" + Thread.currentThread().getName() + "]"
                : "";
        String assocData = (as != null ? as.toString()
                : (extraData != null ? extraData.getProperty("FROM_AET", "<UNKNOWN>") : "<UNKNOWN>") + "+0");
        LOG.info("{}{}: {} {} >> {}",
                new String[] { assocData, threadName, dialog, command, new Gson().toJson(message) });
    }

    @Override
    public synchronized void logMessage(DicomClientMetaInfo metaInfo, String command, Object message) {
        logMessage(metaInfo, command, message, "EMSOW-DIALOG");
    }

    @Override
    public synchronized void logMultilineMessage(DicomClientMetaInfo metaInfo, String command, String header,
            String body, String footer) {
        Association as = metaInfo.getAssoc();
        Properties extraData = metaInfo.getExtraData();
        String threadName = !"main".equals(Thread.currentThread().getName())
                ? " [" + Thread.currentThread().getName() + "]"
                : "";
        String assocData = (as != null ? as.toString()
                : (extraData != null ? extraData.getProperty("FROM_AET", "<UNKNOWN>") : "<UNKNOWN>") + "+0");
        String multilineMsg = "=============== " + assocData + threadName + " " + header + " ===============\n"
                + (body.length() > 1000 ? body.substring(0, 1000) + " ... /TRUNCATED/ ...\n" : body + "\n")
                + "================ " + assocData + threadName + " " + footer + " ================";
        System.out.println(multilineMsg);
    }

    public List<InstanceLocator> calculateMatches(Attributes keys, Association as, String command)
            throws DicomServiceException {
        /*String[] patIDs = keys.getStrings(Tag.PatientID);
        String[] studyIUIDs = keys.getStrings(Tag.StudyInstanceUID);
        String[] seriesIUIDs = keys.getStrings(Tag.SeriesInstanceUID);
        String[] sopIUIDs = keys.getStrings(Tag.SOPInstanceUID);*/

        try {
            DicomClientMetaInfo metaInfo = new DicomClientMetaInfo(as);
            logMessage(metaInfo, command, new GenericWebResponse("1", "Retrieving file list from EMSOW..."));
            keys.setString(Tag.QueryRetrieveLevel, VR.CS, "COUNT");
            List<InstanceLocator> list = new ArrayList<InstanceLocator>();

            UnvWebClient webClient = new UnvWebClient(this.dicomUrl.toString());
            webClient.addListener(this, metaInfo);
            webClient.setBasicParams(this.emsowUsername, this.emsowPassword, command, as);
            webClient.setParam("KEYS", DicomAttribute.getAttributesAsList(keys));

            webClient.sendPostRequest();

            CMoveWebResponse cmwr = webClient.parseJsonResponse(CMoveWebResponse.class);

            this.dicomDownloadUrl = cmwr.getUrl();
            Collection<DicomFileWebData> data = cmwr.getData();
            boolean hasEmptyTsuidOnList = false;
            if (data != null) {
                for (DicomFileWebData d : data) {
                    String tsuid = d.getTransferSyntaxUID();
                    list.add(new InstanceLocator(d.getClassUID(), d.getInstanceUID(), tsuid, d.getFileName()));
                    hasEmptyTsuidOnList = hasEmptyTsuidOnList || (tsuid == null || "".equals(tsuid));
                }
            }

            if (hasEmptyTsuidOnList) {
                logMessage(metaInfo, command, new GenericWebResponse("0",
                        "Some of the files have empty TransferSyntaxUID. They will not be retrieved"));
            }

            return list;

        } catch (IOException e) {
            throw new DicomServiceException(Status.UnableToCalculateNumberOfMatches, e);
        }
    }

    public Attributes calculateStorageCommitmentResult(String calledAET, Attributes actionInfo)
            throws DicomServiceException {
        Sequence requestSeq = actionInfo.getSequence(Tag.ReferencedSOPSequence);
        int size = requestSeq.size();
        String[] sopIUIDs = new String[size];
        Attributes eventInfo = new Attributes(6);
        eventInfo.setString(Tag.RetrieveAETitle, VR.AE, calledAET);
        eventInfo.setString(Tag.StorageMediaFileSetID, VR.SH, ddReader.getFileSetID());
        eventInfo.setString(Tag.StorageMediaFileSetUID, VR.SH, ddReader.getFileSetUID());
        eventInfo.setString(Tag.TransactionUID, VR.UI, actionInfo.getString(Tag.TransactionUID));
        Sequence successSeq = eventInfo.newSequence(Tag.ReferencedSOPSequence, size);
        Sequence failedSeq = eventInfo.newSequence(Tag.FailedSOPSequence, size);
        LinkedHashMap<String, String> map = new LinkedHashMap<String, String>(size * 4 / 3);
        for (int i = 0; i < sopIUIDs.length; i++) {
            Attributes item = requestSeq.get(i);
            map.put(sopIUIDs[i] = item.getString(Tag.ReferencedSOPInstanceUID),
                    item.getString(Tag.ReferencedSOPClassUID));
        }
        IDicomReader ddr = ddReader;
        try {
            Attributes patRec = ddr.findPatientRecord();
            while (patRec != null) {
                Attributes studyRec = ddr.findStudyRecord(patRec);
                while (studyRec != null) {
                    Attributes seriesRec = ddr.findSeriesRecord(studyRec);
                    while (seriesRec != null) {
                        Attributes instRec = ddr.findLowerInstanceRecord(seriesRec, true, sopIUIDs);
                        while (instRec != null) {
                            String iuid = instRec.getString(Tag.ReferencedSOPInstanceUIDInFile);
                            String cuid = map.remove(iuid);
                            if (cuid.equals(instRec.getString(Tag.ReferencedSOPClassUIDInFile)))
                                successSeq.add(refSOP(iuid, cuid, Status.Success));
                            else
                                failedSeq.add(refSOP(iuid, cuid, Status.ClassInstanceConflict));
                            instRec = ddr.findNextInstanceRecord(instRec, true, sopIUIDs);
                        }
                        seriesRec = ddr.findNextSeriesRecord(seriesRec);
                    }
                    studyRec = ddr.findNextStudyRecord(studyRec);
                }
                patRec = ddr.findNextPatientRecord(patRec);
            }
        } catch (IOException e) {
            LOG.info("Failed to M-READ " + dicomDir, e);
            throw new DicomServiceException(Status.ProcessingFailure, e);
        }
        for (Map.Entry<String, String> entry : map.entrySet()) {
            failedSeq.add(refSOP(entry.getKey(), entry.getValue(), Status.NoSuchObjectInstance));
        }
        if (failedSeq.isEmpty())
            eventInfo.remove(Tag.FailedSOPSequence);
        return eventInfo;
    }

    boolean addDicomDirRecords(Association as, Attributes ds, Attributes fmi, File f) throws IOException {
        DicomDirWriter ddWriter = getDicomDirWriter();
        RecordFactory recFact = getRecordFactory();
        String pid = ds.getString(Tag.PatientID, null);
        String styuid = ds.getString(Tag.StudyInstanceUID, null);
        String seruid = ds.getString(Tag.SeriesInstanceUID, null);
        String iuid = fmi.getString(Tag.MediaStorageSOPInstanceUID, null);
        if (pid == null)
            ds.setString(Tag.PatientID, VR.LO, pid = styuid);

        Attributes patRec = ddWriter.findPatientRecord(pid);
        if (patRec == null) {
            patRec = recFact.createRecord(RecordType.PATIENT, null, ds, null, null);
            ddWriter.addRootDirectoryRecord(patRec);
        }
        Attributes studyRec = ddWriter.findStudyRecord(patRec, styuid);
        if (studyRec == null) {
            studyRec = recFact.createRecord(RecordType.STUDY, null, ds, null, null);
            ddWriter.addLowerDirectoryRecord(patRec, studyRec);
        }
        Attributes seriesRec = ddWriter.findSeriesRecord(studyRec, seruid);
        if (seriesRec == null) {
            seriesRec = recFact.createRecord(RecordType.SERIES, null, ds, null, null);
            ddWriter.addLowerDirectoryRecord(studyRec, seriesRec);
        }
        Attributes instRec = ddWriter.findLowerInstanceRecord(seriesRec, false, iuid);
        if (instRec != null)
            return false;

        instRec = recFact.createRecord(ds, fmi, ddWriter.toFileIDs(f));
        ddWriter.addLowerDirectoryRecord(seriesRec, instRec);
        ddWriter.commit();
        return true;
    }

    private static Attributes refSOP(String iuid, String cuid, int failureReason) {
        Attributes attrs = new Attributes(3);
        attrs.setString(Tag.ReferencedSOPClassUID, VR.UI, cuid);
        attrs.setString(Tag.ReferencedSOPInstanceUID, VR.UI, iuid);
        if (failureReason != Status.Success)
            attrs.setInt(Tag.FailureReason, VR.US, failureReason);
        return attrs;
    }

}