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.storescu;
040
041import java.io.*;
042import java.security.GeneralSecurityException;
043import java.text.MessageFormat;
044import java.util.*;
045import java.util.concurrent.ExecutorService;
046import java.util.concurrent.Executors;
047import java.util.concurrent.ScheduledExecutorService;
048
049import javax.xml.parsers.ParserConfigurationException;
050
051import org.apache.commons.cli.*;
052import org.dcm4che3.data.Attributes;
053import org.dcm4che3.data.Tag;
054import org.dcm4che3.data.UID;
055import org.dcm4che3.imageio.codec.Decompressor;
056import org.dcm4che3.io.DicomInputStream;
057import org.dcm4che3.io.DicomInputStream.IncludeBulkData;
058import org.dcm4che3.io.SAXReader;
059import org.dcm4che3.net.ApplicationEntity;
060import org.dcm4che3.net.Association;
061import org.dcm4che3.net.Connection;
062import org.dcm4che3.net.DataWriterAdapter;
063import org.dcm4che3.net.Device;
064import org.dcm4che3.net.DimseRSP;
065import org.dcm4che3.net.DimseRSPHandler;
066import org.dcm4che3.net.IncompatibleConnectionException;
067import org.dcm4che3.net.InputStreamDataWriter;
068import org.dcm4che3.net.Status;
069import org.dcm4che3.net.pdu.AAssociateRQ;
070import org.dcm4che3.net.pdu.PresentationContext;
071import org.dcm4che3.tool.common.CLIUtils;
072import org.dcm4che3.tool.common.DicomFiles;
073import org.dcm4che3.util.SafeClose;
074import org.dcm4che3.util.StringUtils;
075import org.dcm4che3.util.TagUtils;
076import org.xml.sax.SAXException;
077
078/**
079 * @author Gunter Zeilinger <gunterze@gmail.com>
080 * @author Michael Backhaus <michael.backhaus@agfa.com>
081 */
082public class StoreSCU {
083
084    public interface RSPHandlerFactory {
085
086        DimseRSPHandler createDimseRSPHandler(File f);
087    }
088
089    private static ResourceBundle rb = ResourceBundle
090            .getBundle("org.dcm4che3.tool.storescu.messages");
091
092    private final ApplicationEntity ae;
093    private final Connection remote;
094    private final AAssociateRQ rq = new AAssociateRQ();
095    private final RelatedGeneralSOPClasses relSOPClasses = new RelatedGeneralSOPClasses();
096    private Attributes attrs;
097    private String uidSuffix;
098    private boolean relExtNeg;
099    private int priority;
100    private String tmpPrefix = "storescu-";
101    private String tmpSuffix;
102    private File tmpDir;
103    private File tmpFile;
104    private String inputFile;
105    private Association as;
106
107    private long totalSize;
108    private int filesScanned;
109    private int filesSent;
110
111    private RSPHandlerFactory rspHandlerFactory = new RSPHandlerFactory() {
112
113        @Override
114        public DimseRSPHandler createDimseRSPHandler(final File f) {
115
116            return new DimseRSPHandler(as.nextMessageID()) {
117
118                @Override
119                public void onDimseRSP(Association as, Attributes cmd,
120                        Attributes data) {
121                    super.onDimseRSP(as, cmd, data);
122                    StoreSCU.this.onCStoreRSP(cmd, f);
123                }
124            };
125        }
126    };
127
128    public StoreSCU(ApplicationEntity ae) throws IOException {
129        this.remote = new Connection();
130        this.ae = ae;
131        rq.addPresentationContext(new PresentationContext(1,
132                UID.VerificationSOPClass, UID.ImplicitVRLittleEndian));
133    }
134
135    public void setRspHandlerFactory(RSPHandlerFactory rspHandlerFactory) {
136        this.rspHandlerFactory = rspHandlerFactory;
137    }
138
139    public AAssociateRQ getAAssociateRQ() {
140        return rq;
141    }
142
143    public Connection getRemoteConnection() {
144        return remote;
145    }
146
147    public Attributes getAttributes() {
148        return attrs;
149    }
150
151    public void setAttributes(Attributes attrs) {
152        this.attrs = attrs;
153    }
154
155    public void setTmpFile(File tmpFile) {
156        this.tmpFile = tmpFile;
157    }
158
159    public final void setPriority(int priority) {
160        this.priority = priority;
161    }
162
163    public final void setUIDSuffix(String uidSuffix) {
164        this.uidSuffix = uidSuffix;
165    }
166
167    public final void setTmpFilePrefix(String prefix) {
168        this.tmpPrefix = prefix;
169    }
170
171    public final void setTmpFileSuffix(String suffix) {
172        this.tmpSuffix = suffix;
173    }
174
175    public final void setTmpFileDirectory(File tmpDir) {
176        this.tmpDir = tmpDir;
177    }
178
179    private static CommandLine parseComandLine(String[] args)
180            throws ParseException {
181        Options opts = new Options();
182        CLIUtils.addConnectOption(opts);
183        CLIUtils.addBindOption(opts, "STORESCU");
184        CLIUtils.addAEOptions(opts);
185        CLIUtils.addResponseTimeoutOption(opts);
186        CLIUtils.addPriorityOption(opts);
187        CLIUtils.addCommonOptions(opts);
188        addTmpFileOptions(opts);
189        addRelatedSOPClassOptions(opts);
190        addAttributesOption(opts);
191        addUIDSuffixOption(opts);
192        addInputFileOption(opts);
193        return CLIUtils.parseComandLine(args, opts, rb, StoreSCU.class);
194    }
195
196    @SuppressWarnings("static-access")
197    private static void addAttributesOption(Options opts) {
198        opts.addOption(OptionBuilder.hasArgs().withArgName("[seq/]attr=value")
199                .withValueSeparator('=').withDescription(rb.getString("set"))
200                .create("s"));
201    }
202
203    @SuppressWarnings("static-access")
204    public static void addUIDSuffixOption(Options opts) {
205        opts.addOption(OptionBuilder.hasArg().withArgName("suffix")
206                .withDescription(rb.getString("uid-suffix"))
207                .withLongOpt("uid-suffix").create(null));
208    }
209
210    @SuppressWarnings("static-access")
211    public static void addTmpFileOptions(Options opts) {
212        opts.addOption(OptionBuilder.hasArg().withArgName("directory")
213                .withDescription(rb.getString("tmp-file-dir"))
214                .withLongOpt("tmp-file-dir").create(null));
215        opts.addOption(OptionBuilder.hasArg().withArgName("prefix")
216                .withDescription(rb.getString("tmp-file-prefix"))
217                .withLongOpt("tmp-file-prefix").create(null));
218        opts.addOption(OptionBuilder.hasArg().withArgName("suffix")
219                .withDescription(rb.getString("tmp-file-suffix"))
220                .withLongOpt("tmp-file-suffix").create(null));
221    }
222
223    @SuppressWarnings("static-access")
224    private static void addRelatedSOPClassOptions(Options opts) {
225        opts.addOption(null, "rel-ext-neg", false, rb.getString("rel-ext-neg"));
226        opts.addOption(OptionBuilder.hasArg().withArgName("file|url")
227                .withDescription(rb.getString("rel-sop-classes"))
228                .withLongOpt("rel-sop-classes").create(null));
229    }
230
231    @SuppressWarnings("static-access")
232    private static void addInputFileOption(Options opts) {
233        opts.addOption(OptionBuilder.hasArg().withArgName("file").withDescription(rb.getString("input-file"))
234                .withLongOpt("input-file").create(null));
235
236    }
237
238    @SuppressWarnings("unchecked")
239    public static void main(String[] args) {
240        long t1, t2;
241        try {
242            CommandLine cl = parseComandLine(args);
243            Device device = new Device("storescu");
244            Connection conn = new Connection();
245            device.addConnection(conn);
246            ApplicationEntity ae = new ApplicationEntity("STORESCU");
247            device.addApplicationEntity(ae);
248            ae.addConnection(conn);
249            StoreSCU main = new StoreSCU(ae);
250            configureTmpFile(main, cl);
251            configureInputFile(main, cl);
252            CLIUtils.configureConnect(main.remote, main.rq, cl);
253            CLIUtils.configureBind(conn, ae, cl);
254            CLIUtils.configure(conn, cl);
255            main.remote.setTlsProtocols(conn.tlsProtocols());
256            main.remote.setTlsCipherSuites(conn.getTlsCipherSuites());
257            configureRelatedSOPClass(main, cl);
258            main.setAttributes(new Attributes());
259            CLIUtils.addAttributes(main.attrs, cl.getOptionValues("s"));
260            main.setUIDSuffix(cl.getOptionValue("uid-suffix"));
261            main.setPriority(CLIUtils.priorityOf(cl));
262            List<String> argList = cl.getArgList();
263            boolean echo = argList.isEmpty() && main.inputFile == null;
264            if (!echo) {
265                System.out.println(rb.getString("scanning"));
266                t1 = System.currentTimeMillis();
267                if (main.inputFile != null) {
268                    main.scanFiles(getFilePathsFromInputFile(main.inputFile));
269                } else {
270                    main.scanFiles(argList);
271                }
272                t2 = System.currentTimeMillis();
273                int n = main.filesScanned;
274                System.out.println();
275                if (n == 0)
276                    return;
277                System.out.println(MessageFormat.format(
278                        rb.getString("scanned"), n, (t2 - t1) / 1000F,
279                        (t2 - t1) / n));
280            }
281            ExecutorService executorService = Executors
282                    .newSingleThreadExecutor();
283            ScheduledExecutorService scheduledExecutorService = Executors
284                    .newSingleThreadScheduledExecutor();
285            device.setExecutor(executorService);
286            device.setScheduledExecutor(scheduledExecutorService);
287            try {
288                t1 = System.currentTimeMillis();
289                main.open();
290                t2 = System.currentTimeMillis();
291                System.out.println(MessageFormat.format(
292                        rb.getString("connected"), main.as.getRemoteAET(), t2
293                                - t1));
294                if (echo)
295                    main.echo();
296                else {
297                    t1 = System.currentTimeMillis();
298                    main.sendFiles();
299                    t2 = System.currentTimeMillis();
300                }
301            } finally {
302                main.close();
303                executorService.shutdown();
304                scheduledExecutorService.shutdown();
305            }
306            if (main.filesScanned > 0) {
307                float s = (t2 - t1) / 1000F;
308                float mb = main.totalSize / 1048576F;
309                System.out.println(MessageFormat.format(rb.getString("sent"),
310                        main.filesSent, mb, s, mb / s));
311            }
312        } catch (ParseException e) {
313            System.err.println("storescu: " + e.getMessage());
314            System.err.println(rb.getString("try"));
315            System.exit(2);
316        } catch (Exception e) {
317            System.err.println("storescu: " + e.getMessage());
318            e.printStackTrace();
319            System.exit(2);
320        }
321    }
322
323    public static String uidSuffixOf(CommandLine cl) {
324        return cl.getOptionValue("uid-suffix");
325    }
326
327    private static void configureTmpFile(StoreSCU storescu, CommandLine cl) {
328        if (cl.hasOption("tmp-file-dir"))
329            storescu.setTmpFileDirectory(new File(cl
330                    .getOptionValue("tmp-file-dir")));
331        storescu.setTmpFilePrefix(cl.getOptionValue("tmp-file-prefix",
332                "storescu-"));
333        storescu.setTmpFileSuffix(cl.getOptionValue("tmp-file-suffix"));
334    }
335
336    private static void configureInputFile(StoreSCU storescu, CommandLine cl) {
337        if (cl.hasOption("input-file")) {
338            storescu.inputFile = cl.getOptionValue("input-file");
339        }
340    }
341
342    public static void configureRelatedSOPClass(StoreSCU storescu,
343            CommandLine cl) throws IOException {
344        if (cl.hasOption("rel-ext-neg")) {
345            storescu.enableSOPClassRelationshipExtNeg(true);
346            Properties p = new Properties();
347            CLIUtils.loadProperties(
348                    cl.getOptionValue("rel-sop-classes", "resource:rel-sop-classes.properties"),
349                    p);
350            storescu.relSOPClasses.init(p);
351        }
352    }
353
354    private static List<String> getFilePathsFromInputFile(String inputFile) throws IOException {
355        BufferedReader br = null;
356        List<String> inputFiles = new ArrayList<String>();
357
358        try {
359            br = new BufferedReader(new FileReader(inputFile));
360            String line;
361            while ((line = br.readLine()) != null) {
362                inputFiles.add(line);
363            }
364        } finally {
365            SafeClose.close(br);
366        }
367
368        return inputFiles;
369    }
370
371    public final void enableSOPClassRelationshipExtNeg(boolean enable) {
372        relExtNeg = enable;
373    }
374
375    public void scanFiles(List<String> fnames) throws IOException {
376        this.scanFiles(fnames, true);
377    }
378
379    public void scanFiles(List<String> fnames, boolean printout)
380            throws IOException {
381        tmpFile = File.createTempFile(tmpPrefix, tmpSuffix, tmpDir);
382        tmpFile.deleteOnExit();
383        final BufferedWriter fileInfos = new BufferedWriter(
384                new OutputStreamWriter(new FileOutputStream(tmpFile)));
385        try {
386            DicomFiles.scan(fnames, printout, new DicomFiles.Callback() {
387
388                @Override
389                public boolean dicomFile(File f, Attributes fmi, long dsPos,
390                        Attributes ds) throws IOException {
391                    if (!addFile(fileInfos, f, dsPos, fmi, ds))
392                        return false;
393
394                    filesScanned++;
395                    return true;
396                }
397            });
398        } finally {
399            fileInfos.close();
400        }
401    }
402
403    public void sendFiles() throws IOException {
404        BufferedReader fileInfos = new BufferedReader(new InputStreamReader(
405                new FileInputStream(tmpFile)));
406        try {
407            String line;
408            while (as.isReadyForDataTransfer()
409                    && (line = fileInfos.readLine()) != null) {
410                String[] ss = StringUtils.split(line, '\t');
411                try {
412                    send(new File(ss[4]), Long.parseLong(ss[3]), ss[1], ss[0],
413                            ss[2]);
414                } catch (Exception e) {
415                    e.printStackTrace();
416                }
417            }
418
419            as.releaseGracefully();
420
421        } finally {
422            SafeClose.close(fileInfos);
423        }
424    }
425
426    public boolean addFile(BufferedWriter fileInfos, File f, long endFmi,
427            Attributes fmi, Attributes ds) throws IOException {
428        String cuid = fmi.getString(Tag.MediaStorageSOPClassUID);
429        String iuid = fmi.getString(Tag.MediaStorageSOPInstanceUID);
430        String ts = fmi.getString(Tag.TransferSyntaxUID);
431        if (cuid == null || iuid == null)
432            return false;
433
434        fileInfos.write(iuid);
435        fileInfos.write('\t');
436        fileInfos.write(cuid);
437        fileInfos.write('\t');
438        fileInfos.write(ts);
439        fileInfos.write('\t');
440        fileInfos.write(Long.toString(endFmi));
441        fileInfos.write('\t');
442        fileInfos.write(f.getPath());
443        fileInfos.newLine();
444
445        if (rq.containsPresentationContextFor(cuid, ts))
446            return true;
447
448        if (!rq.containsPresentationContextFor(cuid)) {
449            if (relExtNeg)
450                rq.addCommonExtendedNegotiation(relSOPClasses
451                        .getCommonExtendedNegotiation(cuid));
452            if (!ts.equals(UID.ExplicitVRLittleEndian))
453                rq.addPresentationContext(new PresentationContext(rq
454                        .getNumberOfPresentationContexts() * 2 + 1, cuid,
455                        UID.ExplicitVRLittleEndian));
456            if (!ts.equals(UID.ImplicitVRLittleEndian))
457                rq.addPresentationContext(new PresentationContext(rq
458                        .getNumberOfPresentationContexts() * 2 + 1, cuid,
459                        UID.ImplicitVRLittleEndian));
460        }
461        rq.addPresentationContext(new PresentationContext(rq
462                .getNumberOfPresentationContexts() * 2 + 1, cuid, ts));
463        return true;
464    }
465
466    public Attributes echo() throws IOException, InterruptedException {
467        DimseRSP response = as.cecho();
468        response.next();
469        return response.getCommand();
470    }
471
472    public void send(final File f, long fmiEndPos, String cuid, String iuid,
473            String filets) throws IOException, InterruptedException,
474            ParserConfigurationException, SAXException {
475        String ts = selectTransferSyntax(cuid, filets);
476
477        if (f.getName().endsWith(".xml")) {
478            Attributes parsedDicomFile = SAXReader.parse(new FileInputStream(f));
479            if (CLIUtils.updateAttributes(parsedDicomFile, attrs, uidSuffix))
480                iuid = parsedDicomFile.getString(Tag.SOPInstanceUID);
481            if (!ts.equals(filets)) {
482                Decompressor.decompress(parsedDicomFile, filets);
483            }
484            as.cstore(cuid, iuid, priority,
485                    new DataWriterAdapter(parsedDicomFile), ts,
486                    rspHandlerFactory.createDimseRSPHandler(f));
487        } else {
488            if (uidSuffix == null && attrs.isEmpty() && ts.equals(filets)) {
489                FileInputStream in = new FileInputStream(f);
490                try {
491                    in.skip(fmiEndPos);
492                    InputStreamDataWriter data = new InputStreamDataWriter(in);
493                    as.cstore(cuid, iuid, priority, data, ts,
494                            rspHandlerFactory.createDimseRSPHandler(f));
495                } finally {
496                    SafeClose.close(in);
497                }
498            } else {
499                DicomInputStream in = new DicomInputStream(f);
500                try {
501                    in.setIncludeBulkData(IncludeBulkData.URI);
502                    Attributes data = in.readDataset(-1, -1);
503                    if (CLIUtils.updateAttributes(data, attrs, uidSuffix))
504                        iuid = data.getString(Tag.SOPInstanceUID);
505                    if (!ts.equals(filets)) {
506                        Decompressor.decompress(data, filets);
507                    }
508                    as.cstore(cuid, iuid, priority,
509                            new DataWriterAdapter(data), ts,
510                            rspHandlerFactory.createDimseRSPHandler(f));
511                } finally {
512                    SafeClose.close(in);
513                }
514            }
515        }
516    }
517
518    private String selectTransferSyntax(String cuid, String filets) {
519        Set<String> tss = as.getTransferSyntaxesFor(cuid);
520        if (tss.contains(filets))
521            return filets;
522
523        if (tss.contains(UID.ExplicitVRLittleEndian))
524            return UID.ExplicitVRLittleEndian;
525
526        return UID.ImplicitVRLittleEndian;
527    }
528
529    public void close() throws IOException, InterruptedException {
530        if (as != null) {
531            if (as.isReadyForDataTransfer())
532                as.release();
533            as.waitForSocketClose();
534        }
535    }
536
537    public void open() throws IOException, InterruptedException,
538            IncompatibleConnectionException, GeneralSecurityException {
539        as = ae.connect(remote, rq);
540    }
541
542    private void onCStoreRSP(Attributes cmd, File f) {
543        int status = cmd.getInt(Tag.Status, -1);
544        switch (status) {
545        case Status.Success:
546            totalSize += f.length();
547            ++filesSent;
548            System.out.print('.');
549            break;
550        case Status.CoercionOfDataElements:
551        case Status.ElementsDiscarded:
552        case Status.DataSetDoesNotMatchSOPClassWarning:
553            totalSize += f.length();
554            ++filesSent;
555            System.err.println(MessageFormat.format(rb.getString("warning"),
556                    TagUtils.shortToHexString(status), f));
557            System.err.println(cmd);
558            break;
559        default:
560            System.out.print('E');
561            System.err.println(MessageFormat.format(rb.getString("error"),
562                    TagUtils.shortToHexString(status), f));
563            System.err.println(cmd);
564        }
565    }
566}