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.findscu;
040
041import java.io.BufferedOutputStream;
042import java.io.File;
043import java.io.FileOutputStream;
044import java.io.IOException;
045import java.io.OutputStream;
046import java.security.GeneralSecurityException;
047import java.text.DecimalFormat;
048import java.text.MessageFormat;
049import java.util.EnumSet;
050import java.util.List;
051import java.util.ResourceBundle;
052import java.util.Set;
053import java.util.concurrent.ExecutorService;
054import java.util.concurrent.Executors;
055import java.util.concurrent.ScheduledExecutorService;
056import java.util.concurrent.atomic.AtomicInteger;
057
058import javax.xml.transform.OutputKeys;
059import javax.xml.transform.Templates;
060import javax.xml.transform.TransformerFactory;
061import javax.xml.transform.sax.SAXTransformerFactory;
062import javax.xml.transform.sax.TransformerHandler;
063import javax.xml.transform.stream.StreamResult;
064import javax.xml.transform.stream.StreamSource;
065
066import org.apache.commons.cli.CommandLine;
067import org.apache.commons.cli.OptionBuilder;
068import org.apache.commons.cli.Options;
069import org.apache.commons.cli.ParseException;
070import org.dcm4che3.data.Attributes;
071import org.dcm4che3.data.Tag;
072import org.dcm4che3.data.UID;
073import org.dcm4che3.data.VR;
074import org.dcm4che3.io.DicomInputStream;
075import org.dcm4che3.io.DicomOutputStream;
076import org.dcm4che3.io.SAXWriter;
077import org.dcm4che3.net.*;
078import org.dcm4che3.net.pdu.AAssociateRQ;
079import org.dcm4che3.net.pdu.ExtendedNegotiation;
080import org.dcm4che3.net.pdu.PresentationContext;
081import org.dcm4che3.tool.common.CLIUtils;
082import org.dcm4che3.util.SafeClose;
083import org.dcm4che3.util.StringUtils;
084
085/**
086 * The findscu application implements a Service Class User (SCU) for the
087 * Query/Retrieve, the Modality Worklist Management, the Unified Worklist and
088 * Procedure Step, the Hanging Protocol Query/Retrieve and the Color Palette
089 * Query/Retrieve Service Class. findscu only supports query functionality using
090 * the C-FIND message. It sends query keys to an Service Class Provider (SCP)
091 * and waits for responses.
092 * 
093 * @author Gunter Zeilinger <gunterze@gmail.com>
094 */
095public class FindSCU {
096
097    public static enum InformationModel {
098        PatientRoot(UID.PatientRootQueryRetrieveInformationModelFIND, "STUDY"),
099        StudyRoot(UID.StudyRootQueryRetrieveInformationModelFIND, "STUDY"),
100        PatientStudyOnly(UID.PatientStudyOnlyQueryRetrieveInformationModelFINDRetired, "STUDY"),
101        MWL(UID.ModalityWorklistInformationModelFIND, null),
102        UPSPull(UID.UnifiedProcedureStepPullSOPClass, null),
103        UPSWatch(UID.UnifiedProcedureStepWatchSOPClass, null),
104        HangingProtocol(UID.HangingProtocolInformationModelFIND, null),
105        ColorPalette(UID.ColorPaletteQueryRetrieveInformationModelFIND, null);
106
107        final String cuid;
108        final String level;
109
110        InformationModel(String cuid, String level) {
111            this.cuid = cuid;
112            this.level = level;
113       }
114
115        public void adjustQueryOptions(EnumSet<QueryOption> queryOptions) {
116            if (level == null) {
117                queryOptions.add(QueryOption.RELATIONAL);
118                queryOptions.add(QueryOption.DATETIME);
119            }
120        }
121
122        public String getCuid() {
123            return cuid;
124        }
125    }
126
127    private static ResourceBundle rb =
128        ResourceBundle.getBundle("org.dcm4che3.tool.findscu.messages");
129    private static SAXTransformerFactory saxtf;
130
131    private Device device = new Device("findscu");
132    private ApplicationEntity ae = new ApplicationEntity("FINDSCU");
133    private final Connection conn = new Connection();
134    private final Connection remote = new Connection();
135    private final AAssociateRQ rq = new AAssociateRQ();
136    private int priority;
137    private int cancelAfter;
138    private InformationModel model;
139    private static String[] modelUIDandTS;
140    
141    private File outDir;
142    private DecimalFormat outFileFormat;
143    private int[] inFilter;
144    private final Attributes keys = new Attributes();
145
146    private boolean catOut = false;
147    private boolean xml = false;
148    private boolean xmlIndent = false;
149    private boolean xmlIncludeKeyword = true;
150    private boolean xmlIncludeNamespaceDeclaration = false;
151    private File xsltFile;
152    private Templates xsltTpls;
153    private OutputStream out;
154
155    private Association as;
156    private final AtomicInteger totNumMatches = new AtomicInteger();
157    
158    private long tStartCFind;
159
160    public FindSCU() throws IOException {
161        device.addConnection(conn);
162        device.addApplicationEntity(ae);
163        ae.addConnection(conn);
164    }
165    public FindSCU(ApplicationEntity appEntity) throws IOException {
166        this.ae = appEntity;
167        this.device = this.ae.getDevice();
168
169    }
170    public final void setPriority(int priority) {
171        this.priority = priority;
172    }
173
174    public final void setInformationModel(InformationModel model, String[] tss,
175            EnumSet<QueryOption> queryOptions) {
176       this.model = model;
177       rq.addPresentationContext(new PresentationContext(1, model.cuid, tss));
178       if (!queryOptions.isEmpty()) {
179           model.adjustQueryOptions(queryOptions);
180           rq.addExtendedNegotiation(new ExtendedNegotiation(model.cuid, 
181                   QueryOption.toExtendedNegotiationInformation(queryOptions)));
182       }
183       if (model.level != null)
184           addLevel(model.level);
185    }
186
187    public void addLevel(String s) {
188        keys.setString(Tag.QueryRetrieveLevel, VR.CS, s);
189    }
190
191    public final void setCancelAfter(int cancelAfter) {
192        this.cancelAfter = cancelAfter;
193    }
194
195    public final void setOutputDirectory(File outDir) {
196        outDir.mkdirs();
197        this.outDir = outDir;
198    }
199
200    public final void setOutputFileFormat(String outFileFormat) {
201        this.outFileFormat = new DecimalFormat(outFileFormat);
202    }
203
204    public final void setXSLT(File xsltFile) {
205        this.xsltFile = xsltFile;
206    }
207
208    public final void setXML(boolean xml) {
209        this.xml = xml;
210    }
211
212    public final void setXMLIndent(boolean indent) {
213        this.xmlIndent = indent;
214    }
215
216    public final void setXMLIncludeKeyword(boolean includeKeyword) {
217        this.xmlIncludeKeyword = includeKeyword;
218    }
219
220    public final void setXMLIncludeNamespaceDeclaration(
221            boolean includeNamespaceDeclaration) {
222        this.xmlIncludeNamespaceDeclaration = includeNamespaceDeclaration;
223    }
224
225    public final void setConcatenateOutputFiles(boolean catOut) {
226        this.catOut = catOut;
227    }
228
229    public final void setInputFilter(int[] inFilter) {
230        this.inFilter = inFilter;
231    }
232
233    private static CommandLine parseComandLine(String[] args)
234                throws ParseException {
235            Options opts = new Options();
236            addServiceClassOptions(opts);
237            addKeyOptions(opts);
238            addOutputOptions(opts);
239            addQueryLevelOption(opts);
240            addCancelOption(opts);
241            CLIUtils.addConnectOption(opts);
242            CLIUtils.addBindOption(opts, "FINDSCU");
243            CLIUtils.addAEOptions(opts);
244            CLIUtils.addResponseTimeoutOption(opts);
245            CLIUtils.addPriorityOption(opts);
246            CLIUtils.addCommonOptions(opts);
247            return CLIUtils.parseComandLine(args, opts, rb, FindSCU.class);
248    }
249
250    @SuppressWarnings("static-access")
251    private static void addServiceClassOptions(Options opts) {
252        opts.addOption(OptionBuilder
253                .hasArg()
254                .withArgName("name")
255                .withDescription(rb.getString("model"))
256                .create("M"));
257        CLIUtils.addTransferSyntaxOptions(opts);
258        opts.addOption(null, "model-uid", true, rb.getString("model-uid"));
259        opts.addOption(null, "relational", false, rb.getString("relational"));
260        opts.addOption(null, "datetime", false, rb.getString("datetime"));
261        opts.addOption(null, "fuzzy", false, rb.getString("fuzzy"));
262        opts.addOption(null, "timezone", false, rb.getString("timezone"));
263    }
264
265    @SuppressWarnings("static-access")
266    private static void addQueryLevelOption(Options opts) {
267        opts.addOption(OptionBuilder
268                .hasArg()
269                .withArgName("PATIENT|STUDY|SERIES|IMAGE")
270                .withDescription(rb.getString("level"))
271                .create("L"));
272   }
273
274    @SuppressWarnings("static-access")
275    private static void addCancelOption(Options opts) {
276        opts.addOption(OptionBuilder
277                .withLongOpt("cancel")
278                .hasArg()
279                .withArgName("num-matches")
280                .withDescription(rb.getString("cancel"))
281                .create());
282    }
283
284    @SuppressWarnings("static-access")
285    private static void addKeyOptions(Options opts) {
286        opts.addOption(OptionBuilder
287                .hasArgs()
288                .withArgName("[seq/]attr=value")
289                .withValueSeparator('=')
290                .withDescription(rb.getString("match"))
291                .create("m"));
292        opts.addOption(OptionBuilder
293                .hasArgs()
294                .withArgName("[seq/]attr")
295                .withDescription(rb.getString("return"))
296                .create("r"));
297        opts.addOption(OptionBuilder
298                .hasArgs()
299                .withArgName("attr")
300                .withDescription(rb.getString("in-attr"))
301                .create("i"));
302    }
303
304    @SuppressWarnings("static-access")
305    private static void addOutputOptions(Options opts) {
306        opts.addOption(OptionBuilder
307                .withLongOpt("out-dir")
308                .hasArg()
309                .withArgName("directory")
310                .withDescription(rb.getString("out-dir"))
311                .create());
312        opts.addOption(OptionBuilder
313                .withLongOpt("out-file")
314                .hasArg()
315                .withArgName("name")
316                .withDescription(rb.getString("out-file"))
317                .create());
318        opts.addOption("X", "xml", false, rb.getString("xml"));
319        opts.addOption(OptionBuilder
320                .withLongOpt("xsl")
321                .hasArg()
322                .withArgName("xsl-file")
323                .withDescription(rb.getString("xsl"))
324                .create("x"));
325        opts.addOption("I", "indent", false, rb.getString("indent"));
326        opts.addOption("K", "no-keyword", false, rb.getString("no-keyword"));
327        opts.addOption(null, "xmlns", false, rb.getString("xmlns"));
328        opts.addOption(null, "out-cat", false, rb.getString("out-cat"));
329    }
330    
331    public ApplicationEntity getApplicationEntity() {
332        return ae;
333    }
334
335    public Connection getRemoteConnection() {
336        return remote;
337    }
338    
339    public AAssociateRQ getAAssociateRQ() {
340        return rq;
341    }
342    
343    public Association getAssociation() {
344        return as;
345    }
346
347    public Device getDevice() {
348        return device;
349    }    
350    
351    public Attributes getKeys() {
352        return keys;
353    }
354
355    @SuppressWarnings("unchecked")
356    public static void main(String[] args) {
357        try {
358            CommandLine cl = parseComandLine(args);
359            FindSCU main = new FindSCU();
360            CLIUtils.configureConnect(main.remote, main.rq, cl);
361            CLIUtils.configureBind(main.conn, main.ae, cl);
362            CLIUtils.configure(main.conn, cl);
363            main.remote.setTlsProtocols(main.conn.tlsProtocols());
364            main.remote.setTlsCipherSuites(main.conn.getTlsCipherSuites());
365            configureServiceClass(main, cl);
366            configureKeys(main, cl);
367            configureOutput(main, cl);
368            configureCancel(main, cl);
369            main.setPriority(CLIUtils.priorityOf(cl));
370            ExecutorService executorService =
371                    Executors.newSingleThreadExecutor();
372            ScheduledExecutorService scheduledExecutorService =
373                    Executors.newSingleThreadScheduledExecutor();
374            main.device.setExecutor(executorService);
375            main.device.setScheduledExecutor(scheduledExecutorService);
376            try {
377                long t1 = System.currentTimeMillis();
378                main.open();
379                long t2 = System.currentTimeMillis();
380                System.out.println("Association opened in "+(t2-t1)+"ms");
381                List<String> argList = cl.getArgList();
382                if (argList.isEmpty())
383                    main.query();
384                else
385                    for (String arg : argList)
386                        main.query(new File(arg));
387            } finally {
388                main.close();
389                executorService.shutdown();
390                scheduledExecutorService.shutdown();
391            }
392       } catch (ParseException e) {
393            System.err.println("findscu: " + e.getMessage());
394            System.err.println(rb.getString("try"));
395            System.exit(2);
396        } catch (Exception e) {
397            System.err.println("findscu: " + e.getMessage());
398            e.printStackTrace();
399            System.exit(2);
400        }
401    }
402
403    private static EnumSet<QueryOption> queryOptionsOf(FindSCU main, CommandLine cl) {
404        EnumSet<QueryOption> queryOptions = EnumSet.noneOf(QueryOption.class);
405        if (cl.hasOption("relational"))
406            queryOptions.add(QueryOption.RELATIONAL);
407        if (cl.hasOption("datetime"))
408            queryOptions.add(QueryOption.DATETIME);
409        if (cl.hasOption("fuzzy"))
410            queryOptions.add(QueryOption.FUZZY);
411        if (cl.hasOption("timezone"))
412            queryOptions.add(QueryOption.TIMEZONE);
413        return queryOptions;
414    }
415
416    private static void configureOutput(FindSCU main, CommandLine cl) {
417        if (cl.hasOption("out-dir"))
418            main.setOutputDirectory(new File(cl.getOptionValue("out-dir")));
419        main.setOutputFileFormat(cl.getOptionValue("out-file", "000'.dcm'"));
420        main.setConcatenateOutputFiles(cl.hasOption("out-cat"));
421        main.setXML(cl.hasOption("X"));
422        if (cl.hasOption("x")) {
423            main.setXML(true);
424            main.setXSLT(new File(cl.getOptionValue("x")));
425        }
426        main.setXMLIndent(cl.hasOption("I"));
427        main.setXMLIncludeKeyword(!cl.hasOption("K"));
428        main.setXMLIncludeNamespaceDeclaration(cl.hasOption("xmlns"));
429    }
430
431    private static void configureCancel(FindSCU main, CommandLine cl) {
432        if (cl.hasOption("cancel"))
433            main.setCancelAfter(Integer.parseInt(cl.getOptionValue("cancel")));
434    }
435
436    private static void configureKeys(FindSCU main, CommandLine cl) {
437        CLIUtils.addEmptyAttributes(main.keys, cl.getOptionValues("r"));
438        CLIUtils.addAttributes(main.keys, cl.getOptionValues("m"));
439        if (cl.hasOption("L"))
440            main.addLevel(cl.getOptionValue("L"));
441        if (cl.hasOption("i"))
442            main.setInputFilter(CLIUtils.toTags(cl.getOptionValues("i")));
443    }
444
445    private static void configureServiceClass(FindSCU main, CommandLine cl) throws ParseException {
446        main.setInformationModel(informationModelOf(cl), 
447                CLIUtils.transferSyntaxesOf(cl), queryOptionsOf(main, cl));
448        if (cl.hasOption("model-uid")) {
449            String cuidAndTS = cl.getOptionValue("model-uid");
450            modelUIDandTS = StringUtils.split(cuidAndTS, '|');
451            main.rq.addPresentationContext(new PresentationContext(3, modelUIDandTS[0], UID.ImplicitVRLittleEndian));
452            main.rq.addPresentationContext(new PresentationContext(5, modelUIDandTS[0], UID.ExplicitVRLittleEndian));
453            for (int i = 1, pcid = 7 ; i < modelUIDandTS.length ; i++) {
454                main.rq.addPresentationContext(new PresentationContext(pcid, modelUIDandTS[0], modelUIDandTS[i]));
455                pcid += 2;
456            }
457        }
458    }
459
460    private static InformationModel informationModelOf(CommandLine cl) throws ParseException {
461        try {
462            return cl.hasOption("M")
463                    ? InformationModel.valueOf(cl.getOptionValue("M"))
464                    : InformationModel.StudyRoot;
465        } catch(IllegalArgumentException e) {
466            throw new ParseException(
467                    MessageFormat.format(
468                            rb.getString("invalid-model-name"),
469                            cl.getOptionValue("M")));
470        }
471    }
472
473    public void open() throws IOException, InterruptedException,
474            IncompatibleConnectionException, GeneralSecurityException {
475        as = ae.connect(remote, rq);
476    }
477
478    public void close() throws IOException, InterruptedException {
479        if (as != null && as.isReadyForDataTransfer()) {
480            as.waitForOutstandingRSP();
481            as.release();
482        }
483        SafeClose.close(out);
484        out = null;
485    }
486
487    public void query(File f) throws IOException, InterruptedException {
488        Attributes attrs;
489        DicomInputStream dis = null;
490        try {
491            dis = new DicomInputStream(f);
492            attrs = dis.readDataset(-1, -1);
493            if (inFilter != null) {
494                attrs = new Attributes(inFilter.length + 1);
495                attrs.addSelected(attrs, inFilter);
496            }
497        } finally {
498            SafeClose.close(dis);
499        }
500        attrs.addAll(keys);
501        query(attrs);
502    }
503
504    
505   public void query() throws IOException, InterruptedException {
506        query(keys);
507    }
508   
509    private void query(Attributes keys) throws IOException, InterruptedException {
510         DimseRSPHandler rspHandler = new DimseRSPHandler(as.nextMessageID()) {
511
512            int cancelAfter = FindSCU.this.cancelAfter;
513            int numMatches;
514
515            @Override
516            public void onDimseRSP(Association as, Attributes cmd,
517                    Attributes data) {
518                System.out.println("####### DimesRSP received after "+(System.currentTimeMillis()-tStartCFind)+"ms");
519                super.onDimseRSP(as, cmd, data);
520                int status = cmd.getInt(Tag.Status, -1);
521                if (Status.isPending(status)) {
522                    FindSCU.this.onResult(data);
523                    ++numMatches;
524                    if (cancelAfter != 0 && numMatches >= cancelAfter)
525                        try {
526                            cancel(as);
527                            cancelAfter = 0;
528                        } catch (IOException e) {
529                            e.printStackTrace();
530                        }
531                }
532            }
533        };
534
535        query(keys, rspHandler);
536    }
537
538    public void query( DimseRSPHandler rspHandler) throws IOException, InterruptedException {
539        query(keys, rspHandler);
540    }
541    
542    private void query(Attributes keys, DimseRSPHandler rspHandler) throws IOException, InterruptedException {
543        String cuid = model.cuid;
544        if (modelUIDandTS != null) {
545            Set<String> ts = as.getTransferSyntaxesFor(modelUIDandTS[0]);
546            if (ts.size() > 0) {
547                cuid = modelUIDandTS[0];
548            }
549        }
550        tStartCFind = System.currentTimeMillis();
551        as.cfind(cuid, priority, keys, null, rspHandler);
552        long t2 = System.currentTimeMillis();
553        System.out.println("C-FIND Request done in "+(t2-tStartCFind)+"ms!");
554    }
555    
556    private void onResult(Attributes data) {
557        int numMatches = totNumMatches.incrementAndGet();
558        if (outDir == null) 
559            return;
560
561        try {
562            if (out == null) {
563                File f = new File(outDir, fname(numMatches));
564                out = new BufferedOutputStream(
565                        new FileOutputStream(f));
566            }
567            if (xml) {
568                writeAsXML(data, out);
569            } else {
570                DicomOutputStream dos = 
571                        new DicomOutputStream(out, UID.ImplicitVRLittleEndian);
572                dos.writeDataset(null, data);
573            }
574            out.flush();
575        } catch (Exception e) {
576            e.printStackTrace();
577            SafeClose.close(out);
578            out = null;
579        } finally {
580            if (!catOut) {
581                SafeClose.close(out);
582                out = null;
583            }
584        }
585    }
586
587    private String fname(int i) {
588        synchronized (outFileFormat) {
589            return outFileFormat.format(i);
590        }
591    }
592
593    private void writeAsXML(Attributes attrs, OutputStream out) throws Exception {
594        TransformerHandler th = getTransformerHandler();
595        th.getTransformer().setOutputProperty(OutputKeys.INDENT,
596                xmlIndent ? "yes" : "no");
597        th.setResult(new StreamResult(out));
598        SAXWriter saxWriter = new SAXWriter(th);
599        saxWriter.setIncludeKeyword(xmlIncludeKeyword);
600        saxWriter.setIncludeNamespaceDeclaration(xmlIncludeNamespaceDeclaration);
601        saxWriter.write(attrs);
602    }
603
604    private TransformerHandler getTransformerHandler() throws Exception {
605        SAXTransformerFactory tf = saxtf;
606        if (tf == null)
607            saxtf = tf = (SAXTransformerFactory) TransformerFactory
608                .newInstance();
609        if (xsltFile == null)
610            return tf.newTransformerHandler();
611
612        Templates tpls = xsltTpls;
613        if (tpls == null)
614            xsltTpls = tpls = tf.newTemplates(new StreamSource(xsltFile));
615
616        return tf.newTransformerHandler(tpls);
617    }
618
619
620}