001/* ***** BEGIN LICENSE BLOCK *****
002 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
003 *
004 * The contents of this file are subject to the Mozilla Public License Version
005 * 1.1 (the "License"); you may not use this file except in compliance with
006 * the License. You may obtain a copy of the License at
007 * http://www.mozilla.org/MPL/
008 *
009 * Software distributed under the License is distributed on an "AS IS" basis,
010 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
011 * for the specific language governing rights and limitations under the
012 * License.
013 *
014 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in
015 * Java(TM), hosted at https://github.com/gunterze/dcm4che.
016 *
017 * The Initial Developer of the Original Code is
018 * Agfa Healthcare.
019 * Portions created by the Initial Developer are Copyright (C) 2011
020 * the Initial Developer. All Rights Reserved.
021 *
022 * Contributor(s):
023 * See @authors listed below
024 *
025 * Alternatively, the contents of this file may be used under the terms of
026 * either the GNU General Public License Version 2 or later (the "GPL"), or
027 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
028 * in which case the provisions of the GPL or the LGPL are applicable instead
029 * of those above. If you wish to allow use of your version of this file only
030 * under the terms of either the GPL or the LGPL, and not to allow others to
031 * use your version of this file under the terms of the MPL, indicate your
032 * decision by deleting the provisions above and replace them with the notice
033 * and other provisions required by the GPL or the LGPL. If you do not delete
034 * the provisions above, a recipient may use your version of this file under
035 * the terms of any one of the MPL, the GPL or the LGPL.
036 *
037 * ***** END LICENSE BLOCK ***** */
038
039package org.dcm4che3.tool.dcmqrscp;
040
041import java.io.File;
042import java.io.IOException;
043import java.util.ArrayList;
044import java.util.EnumSet;
045import java.util.HashMap;
046import java.util.List;
047import java.util.Map;
048import java.util.Properties;
049import java.util.ResourceBundle;
050import java.util.concurrent.ExecutorService;
051import java.util.concurrent.Executors;
052import java.util.concurrent.ScheduledExecutorService;
053
054import org.apache.commons.cli.CommandLine;
055import org.apache.commons.cli.MissingOptionException;
056import org.apache.commons.cli.OptionBuilder;
057import org.apache.commons.cli.Options;
058import org.apache.commons.cli.ParseException;
059import org.dcm4che3.data.Attributes;
060import org.dcm4che3.data.Tag;
061import org.dcm4che3.data.UID;
062import org.dcm4che3.media.DicomDirReader;
063import org.dcm4che3.media.DicomDirWriter;
064import org.dcm4che3.media.RecordFactory;
065import org.dcm4che3.net.ApplicationEntity;
066import org.dcm4che3.net.Association;
067import org.dcm4che3.net.Connection;
068import org.dcm4che3.net.Device;
069import org.dcm4che3.net.Dimse;
070import org.dcm4che3.net.QueryOption;
071import org.dcm4che3.net.Status;
072import org.dcm4che3.net.TransferCapability;
073import org.dcm4che3.net.pdu.AAssociateRQ;
074import org.dcm4che3.net.pdu.ExtendedNegotiation;
075import org.dcm4che3.net.pdu.PresentationContext;
076import org.dcm4che3.net.service.BasicCEchoSCP;
077import org.dcm4che3.net.service.BasicCFindSCP;
078import org.dcm4che3.net.service.BasicCGetSCP;
079import org.dcm4che3.net.service.BasicCMoveSCP;
080import org.dcm4che3.net.service.BasicCStoreSCU;
081import org.dcm4che3.net.service.BasicRetrieveTask;
082import org.dcm4che3.net.service.CStoreSCU;
083import org.dcm4che3.net.service.DicomServiceException;
084import org.dcm4che3.net.service.DicomServiceRegistry;
085import org.dcm4che3.net.service.InstanceLocator;
086import org.dcm4che3.net.service.QueryRetrieveLevel;
087import org.dcm4che3.net.service.QueryTask;
088import org.dcm4che3.net.service.RetrieveTask;
089import org.dcm4che3.tool.common.CLIUtils;
090import org.dcm4che3.tool.common.FilesetInfo;
091import org.dcm4che3.util.AttributesFormat;
092import org.dcm4che3.util.StringUtils;
093import org.dcm4che3.util.UIDUtils;
094import org.slf4j.Logger;
095import org.slf4j.LoggerFactory;
096
097/**
098 * @author Gunter Zeilinger <gunterze@gmail.com>
099 *
100 */
101public class DcmQRSCP<T extends InstanceLocator> {
102
103    static final Logger LOG = LoggerFactory.getLogger(DcmQRSCP.class);
104
105    private static final String[] PATIENT_ROOT_LEVELS = { "PATIENT", "STUDY",
106            "SERIES", "IMAGE" };
107    private static final String[] STUDY_ROOT_LEVELS = { "STUDY", "SERIES",
108            "IMAGE" };
109    private static final String[] PATIENT_STUDY_ONLY_LEVELS = { "PATIENT",
110            "STUDY" };
111    private static ResourceBundle rb = ResourceBundle
112            .getBundle("org.dcm4che3.tool.dcmqrscp.messages");
113
114    private Device device = new Device("dcmqrscp");
115    private ApplicationEntity ae = new ApplicationEntity("*");
116    private final Connection conn = new Connection();
117
118    private File storageDir;
119    private File dicomDir;
120    private AttributesFormat filePathFormat;
121    private RecordFactory recFact;
122    private String availability;
123    private boolean stgCmtOnSameAssoc;
124    private boolean sendPendingCGet;
125    private int sendPendingCMoveInterval;
126    private final FilesetInfo fsInfo = new FilesetInfo();
127    private DicomDirReader ddReader;
128    private DicomDirWriter ddWriter;
129    private HashMap<String, Connection> remoteConnections = new HashMap<String, Connection>();
130    
131    private final class CFindSCPImpl extends BasicCFindSCP {
132
133        private final String[] qrLevels;
134        private final QueryRetrieveLevel rootLevel;
135
136        public CFindSCPImpl(String sopClass, String... qrLevels) {
137            super(sopClass);
138            this.qrLevels = qrLevels;
139            this.rootLevel = QueryRetrieveLevel.valueOf(qrLevels[0]);
140        }
141
142        @Override
143        protected QueryTask calculateMatches(Association as,
144                PresentationContext pc, Attributes rq, Attributes keys)
145                throws DicomServiceException {
146            QueryRetrieveLevel level = QueryRetrieveLevel.valueOf(keys,
147                    qrLevels);
148            level.validateQueryKeys(keys, rootLevel,
149                    rootLevel == QueryRetrieveLevel.IMAGE || relational(as, rq));
150            DicomDirReader ddr = getDicomDirReader();
151            String availability = getInstanceAvailability();
152            switch (level) {
153            case PATIENT:
154                return new PatientQueryTask(as, pc, rq, keys, ddr, availability);
155            case STUDY:
156                return new StudyQueryTask(as, pc, rq, keys, ddr, availability);
157            case SERIES:
158                return new SeriesQueryTask(as, pc, rq, keys, ddr, availability);
159            case IMAGE:
160                return new InstanceQueryTask(as, pc, rq, keys, ddr,
161                        availability);
162            default:
163                assert true;
164            }
165            throw new AssertionError();
166        }
167
168        private boolean relational(Association as, Attributes rq) {
169            String cuid = rq.getString(Tag.AffectedSOPClassUID);
170            ExtendedNegotiation extNeg = as.getAAssociateAC()
171                    .getExtNegotiationFor(cuid);
172            return QueryOption.toOptions(extNeg).contains(
173                    QueryOption.RELATIONAL);
174        }
175    }
176
177    private final class CGetSCPImpl extends BasicCGetSCP {
178
179        private final String[] qrLevels;
180        private final boolean withoutBulkData;
181        private final QueryRetrieveLevel rootLevel;
182
183        public CGetSCPImpl(String sopClass, String... qrLevels) {
184            super(sopClass);
185            this.qrLevels = qrLevels;
186            this.withoutBulkData = qrLevels.length == 0;
187            this.rootLevel = withoutBulkData ? QueryRetrieveLevel.IMAGE
188                    : QueryRetrieveLevel.valueOf(qrLevels[0]);
189        }
190
191        @Override
192        protected RetrieveTask calculateMatches(Association as,
193                PresentationContext pc, Attributes rq, Attributes keys)
194                throws DicomServiceException {
195            QueryRetrieveLevel level = withoutBulkData ? QueryRetrieveLevel.IMAGE
196                    : QueryRetrieveLevel.valueOf(keys, qrLevels);
197            level.validateRetrieveKeys(keys, rootLevel, relational(as, rq));
198            List<T> matches = DcmQRSCP.this
199                    .calculateMatches(keys);
200            if (matches.isEmpty())
201                return null;
202
203            CStoreSCU<T> storescu = new CStoreSCUImpl<T>(withoutBulkData);
204
205            BasicRetrieveTask<T> retrieveTask = new BasicRetrieveTask<T>(
206                    Dimse.C_GET_RQ, as, pc, rq, matches, as, storescu);
207            retrieveTask.setSendPendingRSP(isSendPendingCGet());
208            return retrieveTask;
209        }
210
211        private boolean relational(Association as, Attributes rq) {
212            String cuid = rq.getString(Tag.AffectedSOPClassUID);
213            ExtendedNegotiation extNeg = as.getAAssociateAC()
214                    .getExtNegotiationFor(cuid);
215            return QueryOption.toOptions(extNeg).contains(
216                    QueryOption.RELATIONAL);
217        }
218
219    }
220
221    private final class CMoveSCPImpl extends BasicCMoveSCP {
222
223        private final String[] qrLevels;
224        private final QueryRetrieveLevel rootLevel;
225
226        public CMoveSCPImpl(String sopClass, String... qrLevels) {
227            super(sopClass);
228            this.qrLevels = qrLevels;
229            this.rootLevel = QueryRetrieveLevel.valueOf(qrLevels[0]);
230        }
231
232        @Override
233        protected RetrieveTask calculateMatches(Association as,
234                PresentationContext pc, final Attributes rq, Attributes keys)
235                throws DicomServiceException {
236            QueryRetrieveLevel level = QueryRetrieveLevel.valueOf(keys,
237                    qrLevels);
238            level.validateRetrieveKeys(keys, rootLevel, relational(as, rq));
239            String moveDest = rq.getString(Tag.MoveDestination);
240            final Connection remote = getRemoteConnection(moveDest);
241            if (remote == null)
242                throw new DicomServiceException(Status.MoveDestinationUnknown,
243                        "Move Destination: " + moveDest + " unknown");
244            List<T> matches = DcmQRSCP.this.calculateMatches(keys);
245            if (matches.isEmpty())
246                return null;
247
248            AAssociateRQ aarq = makeAAssociateRQ(as.getLocalAET(), moveDest,
249                    matches);
250            Association storeas = openStoreAssociation(as, remote, aarq);
251
252            BasicRetrieveTask<T> retrieveTask = new BasicRetrieveTask<T>(
253                    Dimse.C_MOVE_RQ, as, pc, rq, matches, storeas,
254                    new BasicCStoreSCU<T>());
255            retrieveTask
256                    .setSendPendingRSPInterval(getSendPendingCMoveInterval());
257            return retrieveTask;
258        }
259
260        private Association openStoreAssociation(Association as,
261                Connection remote, AAssociateRQ aarq)
262                throws DicomServiceException {
263            try {
264                return as.getApplicationEntity().connect(as.getConnection(),
265                        remote, aarq);
266            } catch (Exception e) {
267                throw new DicomServiceException(
268                        Status.UnableToPerformSubOperations, e);
269            }
270        }
271
272        private AAssociateRQ makeAAssociateRQ(String callingAET,
273                String calledAET, List<T> matches) {
274            AAssociateRQ aarq = new AAssociateRQ();
275            aarq.setCalledAET(calledAET);
276            aarq.setCallingAET(callingAET);
277            for (InstanceLocator match : matches) {
278                if (aarq.addPresentationContextFor(match.cuid, match.tsuid)) {
279                    if (!UID.ExplicitVRLittleEndian.equals(match.tsuid))
280                        aarq.addPresentationContextFor(match.cuid,
281                                UID.ExplicitVRLittleEndian);
282                    if (!UID.ImplicitVRLittleEndian.equals(match.tsuid))
283                        aarq.addPresentationContextFor(match.cuid,
284                                UID.ImplicitVRLittleEndian);
285                }
286            }
287            return aarq;
288        }
289
290        private boolean relational(Association as, Attributes rq) {
291            String cuid = rq.getString(Tag.AffectedSOPClassUID);
292            ExtendedNegotiation extNeg = as.getAAssociateAC()
293                    .getExtNegotiationFor(cuid);
294            return QueryOption.toOptions(extNeg).contains(
295                    QueryOption.RELATIONAL);
296        }
297    }
298
299    public DcmQRSCP() throws IOException {
300        device.addConnection(conn);
301        device.addApplicationEntity(ae);
302        ae.setAssociationAcceptor(true);
303        ae.addConnection(conn);
304    }
305    
306    public void init() {
307        device.setDimseRQHandler(createServiceRegistry());
308    }
309    
310    protected void addCStoreSCPService(DicomServiceRegistry serviceRegistry ) {
311        serviceRegistry.addDicomService(new CStoreSCPImpl(ddWriter, filePathFormat, recFact));
312    }
313    
314    protected void addStgCmtSCPService(DicomServiceRegistry serviceRegistry ) {
315        serviceRegistry.addDicomService(new StgCmtSCPImpl(ddReader, remoteConnections,  stgCmtOnSameAssoc, device.getExecutor()));
316    }
317
318    private DicomServiceRegistry createServiceRegistry() {
319        DicomServiceRegistry serviceRegistry = new DicomServiceRegistry();
320        serviceRegistry.addDicomService(new BasicCEchoSCP());
321        
322        addCStoreSCPService(serviceRegistry);
323        addStgCmtSCPService(serviceRegistry);
324        
325        serviceRegistry.addDicomService(new CFindSCPImpl(
326                UID.PatientRootQueryRetrieveInformationModelFIND,
327                PATIENT_ROOT_LEVELS));
328        serviceRegistry.addDicomService(new CFindSCPImpl(
329                UID.StudyRootQueryRetrieveInformationModelFIND,
330                STUDY_ROOT_LEVELS));
331        serviceRegistry.addDicomService(new CFindSCPImpl(
332                UID.PatientStudyOnlyQueryRetrieveInformationModelFINDRetired,
333                PATIENT_STUDY_ONLY_LEVELS));
334        serviceRegistry.addDicomService(new CGetSCPImpl(
335                UID.PatientRootQueryRetrieveInformationModelGET,
336                PATIENT_ROOT_LEVELS));
337        serviceRegistry.addDicomService(new CGetSCPImpl(
338                UID.StudyRootQueryRetrieveInformationModelGET,
339                STUDY_ROOT_LEVELS));
340        serviceRegistry.addDicomService(new CGetSCPImpl(
341                UID.PatientStudyOnlyQueryRetrieveInformationModelGETRetired,
342                PATIENT_STUDY_ONLY_LEVELS));
343        serviceRegistry.addDicomService(new CGetSCPImpl(
344                UID.CompositeInstanceRetrieveWithoutBulkDataGET));
345        serviceRegistry.addDicomService(new CMoveSCPImpl(
346                UID.PatientRootQueryRetrieveInformationModelMOVE,
347                PATIENT_ROOT_LEVELS));
348        serviceRegistry.addDicomService(new CMoveSCPImpl(
349                UID.StudyRootQueryRetrieveInformationModelMOVE,
350                STUDY_ROOT_LEVELS));
351        serviceRegistry.addDicomService(new CMoveSCPImpl(
352                UID.PatientStudyOnlyQueryRetrieveInformationModelMOVERetired,
353                PATIENT_STUDY_ONLY_LEVELS));
354        return serviceRegistry;
355    }
356
357    public final Device getDevice() {
358        return device;
359    }
360    
361    public void setDevice(Device device) {
362        this.device = device;
363    }
364    
365    public void setApplicationEntity(ApplicationEntity ae) {
366        this.ae = ae;
367    }
368
369    public final void setDicomDirectory(File dicomDir) {
370        File storageDir = dicomDir.getParentFile();
371        if (storageDir.mkdirs())
372            System.out.println("M-WRITE " + storageDir);
373        this.storageDir = storageDir;
374        this.dicomDir = dicomDir;
375    }
376
377    public final File getStorageDirectory() {
378        return storageDir;
379    }
380
381    public final AttributesFormat getFilePathFormat() {
382        return filePathFormat;
383    }
384
385    public void setFilePathFormat(String pattern) {
386        this.filePathFormat = new AttributesFormat(pattern);
387    }
388
389    public final File getDicomDirectory() {
390        return dicomDir;
391    }
392
393    public boolean isWriteable() {
394        return storageDir.canWrite();
395    }
396
397    public final void setInstanceAvailability(String availability) {
398        this.availability = availability;
399    }
400
401    public final String getInstanceAvailability() {
402        return availability;
403    }
404
405    public boolean isStgCmtOnSameAssoc() {
406        return stgCmtOnSameAssoc;
407    }
408
409    public void setStgCmtOnSameAssoc(boolean stgCmtOnSameAssoc) {
410        this.stgCmtOnSameAssoc = stgCmtOnSameAssoc;
411    }
412
413    public final void setSendPendingCGet(boolean sendPendingCGet) {
414        this.sendPendingCGet = sendPendingCGet;
415    }
416
417    public final boolean isSendPendingCGet() {
418        return sendPendingCGet;
419    }
420
421    public final void setSendPendingCMoveInterval(int sendPendingCMoveInterval) {
422        this.sendPendingCMoveInterval = sendPendingCMoveInterval;
423    }
424
425    public final int getSendPendingCMoveInterval() {
426        return sendPendingCMoveInterval;
427    }
428
429    public final void setRecordFactory(RecordFactory recFact) {
430        this.recFact = recFact;
431    }
432
433    public final RecordFactory getRecordFactory() {
434        return recFact;
435    }
436
437    private static CommandLine parseComandLine(String[] args)
438            throws ParseException {
439        Options opts = new Options();
440        CLIUtils.addFilesetInfoOptions(opts);
441        CLIUtils.addBindServerOption(opts);
442        CLIUtils.addConnectTimeoutOption(opts);
443        CLIUtils.addAcceptTimeoutOption(opts);
444        CLIUtils.addAEOptions(opts);
445        CLIUtils.addCommonOptions(opts);
446        CLIUtils.addResponseTimeoutOption(opts);
447        addDicomDirOption(opts);
448        addTransferCapabilityOptions(opts);
449        addInstanceAvailabilityOption(opts);
450        addStgCmtOptions(opts);
451        addSendingPendingOptions(opts);
452        addRemoteConnectionsOption(opts);
453        return CLIUtils.parseComandLine(args, opts, rb, DcmQRSCP.class);
454    }
455
456    @SuppressWarnings("static-access")
457    private static void addInstanceAvailabilityOption(Options opts) {
458        opts.addOption(OptionBuilder.hasArg().withArgName("code")
459                .withDescription(rb.getString("availability"))
460                .withLongOpt("availability").create());
461    }
462
463    private static void addStgCmtOptions(Options opts) {
464        opts.addOption(null, "stgcmt-same-assoc", false,
465                rb.getString("stgcmt-same-assoc"));
466    }
467
468    @SuppressWarnings("static-access")
469    private static void addSendingPendingOptions(Options opts) {
470        opts.addOption(null, "pending-cget", false,
471                rb.getString("pending-cget"));
472        opts.addOption(OptionBuilder.hasArg().withArgName("s")
473                .withDescription(rb.getString("pending-cmove"))
474                .withLongOpt("pending-cmove").create());
475    }
476
477    @SuppressWarnings("static-access")
478    private static void addDicomDirOption(Options opts) {
479        opts.addOption(OptionBuilder.hasArg().withArgName("file")
480                .withDescription(rb.getString("dicomdir"))
481                .withLongOpt("dicomdir").create());
482        opts.addOption(OptionBuilder.hasArg().withArgName("pattern")
483                .withDescription(rb.getString("filepath"))
484                .withLongOpt("filepath").create(null));
485    }
486
487    @SuppressWarnings("static-access")
488    private static void addTransferCapabilityOptions(Options opts) {
489        opts.addOption(null, "all-storage", false, rb.getString("all-storage"));
490        opts.addOption(null, "no-storage", false, rb.getString("no-storage"));
491        opts.addOption(null, "no-query", false, rb.getString("no-query"));
492        opts.addOption(null, "no-retrieve", false, rb.getString("no-retrieve"));
493        opts.addOption(null, "relational", false, rb.getString("relational"));
494        opts.addOption(OptionBuilder.hasArg().withArgName("file|url")
495                .withDescription(rb.getString("storage-sop-classes"))
496                .withLongOpt("storage-sop-classes").create());
497        opts.addOption(OptionBuilder.hasArg().withArgName("file|url")
498                .withDescription(rb.getString("query-sop-classes"))
499                .withLongOpt("query-sop-classes").create());
500        opts.addOption(OptionBuilder.hasArg().withArgName("file|url")
501                .withDescription(rb.getString("retrieve-sop-classes"))
502                .withLongOpt("retrieve-sop-classes").create());
503    }
504
505    @SuppressWarnings("static-access")
506    private static void addRemoteConnectionsOption(Options opts) {
507        opts.addOption(OptionBuilder.hasArg().withArgName("file|url")
508                .withDescription(rb.getString("ae-config"))
509                .withLongOpt("ae-config").create());
510    }
511
512    public static void main(String[] args) {
513        try {
514            CommandLine cl = parseComandLine(args);
515            DcmQRSCP<InstanceLocator> main = new DcmQRSCP<InstanceLocator>();
516            CLIUtils.configure(main.fsInfo, cl);
517            CLIUtils.configureBindServer(main.conn, main.ae, cl);
518            CLIUtils.configure(main.conn, cl);
519            configureDicomFileSet(main, cl);
520            configureTransferCapability(main, cl);
521            configureInstanceAvailability(main, cl);
522            configureStgCmt(main, cl);
523            configureSendPending(main, cl);
524            configureRemoteConnections(main, cl);
525            ExecutorService executorService = Executors.newCachedThreadPool();
526            ScheduledExecutorService scheduledExecutorService = Executors
527                    .newSingleThreadScheduledExecutor();
528            main.device.setScheduledExecutor(scheduledExecutorService);
529            main.device.setExecutor(executorService);
530            main.device.bindConnections();
531            main.init();
532        } catch (ParseException e) {
533            System.err.println("dcmqrscp: " + e.getMessage());
534            System.err.println(rb.getString("try"));
535            System.exit(2);
536        } catch (Exception e) {
537            System.err.println("dcmqrscp: " + e.getMessage());
538            e.printStackTrace();
539            System.exit(2);
540        }
541    }
542
543    private static void configureDicomFileSet(DcmQRSCP<InstanceLocator> main, CommandLine cl)
544            throws ParseException {
545        if (!cl.hasOption("dicomdir"))
546            throw new MissingOptionException(rb.getString("missing-dicomdir"));
547        main.setDicomDirectory(new File(cl.getOptionValue("dicomdir")));
548        main.setFilePathFormat(cl.getOptionValue("filepath",
549                "DICOM/{0020000D,hash}/{0020000E,hash}/{00080018,hash}"));
550        main.setRecordFactory(new RecordFactory());
551    }
552
553    private static void configureInstanceAvailability(DcmQRSCP<InstanceLocator> main,
554            CommandLine cl) {
555        main.setInstanceAvailability(cl.getOptionValue("availability"));
556    }
557
558    private static void configureStgCmt(DcmQRSCP<InstanceLocator> main, CommandLine cl) {
559        main.setStgCmtOnSameAssoc(cl.hasOption("stgcmt-same-assoc"));
560    }
561
562    private static void configureSendPending(DcmQRSCP<InstanceLocator> main, CommandLine cl) {
563        main.setSendPendingCGet(cl.hasOption("pending-cget"));
564        if (cl.hasOption("pending-cmove"))
565            main.setSendPendingCMoveInterval(Integer.parseInt(cl
566                    .getOptionValue("pending-cmove")));
567    }
568
569    private static void configureTransferCapability(DcmQRSCP<InstanceLocator> main,
570            CommandLine cl) throws IOException {
571        ApplicationEntity ae = main.ae;
572        EnumSet<QueryOption> queryOptions = cl.hasOption("relational") ? EnumSet
573                .of(QueryOption.RELATIONAL) : EnumSet.noneOf(QueryOption.class);
574        boolean storage = !cl.hasOption("no-storage") && main.isWriteable();
575        if (storage && cl.hasOption("all-storage")) {
576            TransferCapability tc = new TransferCapability(null, "*",
577                    TransferCapability.Role.SCP, "*");
578            tc.setQueryOptions(queryOptions);
579            ae.addTransferCapability(tc);
580        } else {
581            ae.addTransferCapability(new TransferCapability(null,
582                    UID.VerificationSOPClass, TransferCapability.Role.SCP,
583                    UID.ImplicitVRLittleEndian));
584            Properties storageSOPClasses = CLIUtils.loadProperties(cl
585                    .getOptionValue("storage-sop-classes",
586                            "resource:storage-sop-classes.properties"), null);
587            if (storage)
588                addTransferCapabilities(ae, storageSOPClasses,
589                        TransferCapability.Role.SCP, null);
590            if (!cl.hasOption("no-retrieve")) {
591                addTransferCapabilities(ae, storageSOPClasses,
592                        TransferCapability.Role.SCU, null);
593                Properties p = CLIUtils.loadProperties(cl.getOptionValue(
594                        "retrieve-sop-classes",
595                        "resource:retrieve-sop-classes.properties"), null);
596                addTransferCapabilities(ae, p, TransferCapability.Role.SCP,
597                        queryOptions);
598            }
599            if (!cl.hasOption("no-query")) {
600                Properties p = CLIUtils.loadProperties(cl.getOptionValue(
601                        "query-sop-classes",
602                        "resource:query-sop-classes.properties"), null);
603                addTransferCapabilities(ae, p, TransferCapability.Role.SCP,
604                        queryOptions);
605            }
606        }
607        if (storage)
608            main.openDicomDir();
609        else
610            main.openDicomDirForReadOnly();
611    }
612
613    private static void addTransferCapabilities(ApplicationEntity ae,
614            Properties p, TransferCapability.Role role,
615            EnumSet<QueryOption> queryOptions) {
616        for (String cuid : p.stringPropertyNames()) {
617            String ts = p.getProperty(cuid);
618            TransferCapability tc = new TransferCapability(null,
619                    CLIUtils.toUID(cuid), role, CLIUtils.toUIDs(ts));
620            tc.setQueryOptions(queryOptions);
621            ae.addTransferCapability(tc);
622        }
623    }
624
625    private static void configureRemoteConnections(DcmQRSCP<InstanceLocator> main, CommandLine cl)
626            throws Exception {
627        String file = cl.getOptionValue("ae-config", "resource:ae.properties");
628        Properties aeConfig = CLIUtils.loadProperties(file, null);
629        for (Map.Entry<Object, Object> entry : aeConfig.entrySet()) {
630            String aet = (String) entry.getKey();
631            String value = (String) entry.getValue();
632            try {
633                String[] hostPortCiphers = StringUtils.split(value, ':');
634                String[] ciphers = new String[hostPortCiphers.length - 2];
635                System.arraycopy(hostPortCiphers, 2, ciphers, 0, ciphers.length);
636                Connection remote = new Connection();
637                remote.setHostname(hostPortCiphers[0]);
638                remote.setPort(Integer.parseInt(hostPortCiphers[1]));
639                remote.setTlsCipherSuites(ciphers);
640                main.addRemoteConnection(aet, remote);
641            } catch (Exception e) {
642                throw new IllegalArgumentException("Invalid entry in " + file
643                        + ": " + aet + "=" + value);
644            }
645        }
646    }
647
648    final DicomDirReader getDicomDirReader() {
649        return ddReader;
650    }
651    
652    public void setDicomDirReader(DicomDirReader ddReader) {
653        this.ddReader = ddReader;
654    }
655
656    final DicomDirWriter getDicomDirWriter() {
657        return ddWriter;
658    }
659
660    private void openDicomDir() throws IOException {
661        if (!dicomDir.exists())
662            DicomDirWriter.createEmptyDirectory(dicomDir,
663                    UIDUtils.createUIDIfNull(fsInfo.getFilesetUID()),
664                    fsInfo.getFilesetID(), fsInfo.getDescriptorFile(),
665                    fsInfo.getDescriptorFileCharset());
666        ddReader = ddWriter = DicomDirWriter.open(dicomDir);
667    }
668
669    private void openDicomDirForReadOnly() throws IOException {
670        ddReader = new DicomDirReader(dicomDir);
671    }
672
673    public void addRemoteConnection(String aet, Connection remote) {
674        remoteConnections.put(aet, remote);
675    }
676
677    Connection getRemoteConnection(String dest) {
678        return remoteConnections.get(dest);
679    }
680
681    public List<T> calculateMatches(Attributes keys)
682            throws DicomServiceException {
683        try {
684            List<T> list = new ArrayList<T>();
685            String[] patIDs = keys.getStrings(Tag.PatientID);
686            String[] studyIUIDs = keys.getStrings(Tag.StudyInstanceUID);
687            String[] seriesIUIDs = keys.getStrings(Tag.SeriesInstanceUID);
688            String[] sopIUIDs = keys.getStrings(Tag.SOPInstanceUID);
689            DicomDirReader ddr = ddReader;
690            Attributes patRec = ddr.findPatientRecord(patIDs);
691            while (patRec != null) {
692                Attributes studyRec = ddr.findStudyRecord(patRec, studyIUIDs);
693                while (studyRec != null) {
694                    Attributes seriesRec = ddr.findSeriesRecord(studyRec,
695                            seriesIUIDs);
696                    while (seriesRec != null) {
697                        Attributes instRec = ddr.findLowerInstanceRecord(
698                                seriesRec, true, sopIUIDs);
699                        while (instRec != null) {
700                            String cuid = instRec
701                                    .getString(Tag.ReferencedSOPClassUIDInFile);
702                            String iuid = instRec
703                                    .getString(Tag.ReferencedSOPInstanceUIDInFile);
704                            String tsuid = instRec
705                                    .getString(Tag.ReferencedTransferSyntaxUIDInFile);
706                            String[] fileIDs = instRec
707                                    .getStrings(Tag.ReferencedFileID);
708                            String uri = ddr.toFile(fileIDs).toURI().toString();
709                            list.add((T)new InstanceLocator(cuid, iuid, tsuid, uri));
710                            if (sopIUIDs != null && sopIUIDs.length == 1)
711                                break;
712
713                            instRec = ddr.findNextInstanceRecord(instRec, true,
714                                    sopIUIDs);
715                        }
716                        if (seriesIUIDs != null && seriesIUIDs.length == 1)
717                            break;
718
719                        seriesRec = ddr.findNextSeriesRecord(seriesRec,
720                                seriesIUIDs);
721                    }
722                    if (studyIUIDs != null && studyIUIDs.length == 1)
723                        break;
724
725                    studyRec = ddr.findNextStudyRecord(studyRec, studyIUIDs);
726                }
727                if (patIDs != null && patIDs.length == 1)
728                    break;
729
730                patRec = ddr.findNextPatientRecord(patRec, patIDs);
731            }
732            return list;
733        } catch (IOException e) {
734            throw new DicomServiceException(
735                    Status.UnableToCalculateNumberOfMatches, e);
736        }
737    }
738    
739    public Connection getConnection() {
740        return conn;
741    }
742    
743    public ApplicationEntity getApplicationEntity() {
744        return ae;
745    }
746
747}