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.getscu;
040
041import java.io.File;
042import java.io.IOException;
043import java.security.GeneralSecurityException;
044import java.text.MessageFormat;
045import java.util.List;
046import java.util.Map.Entry;
047import java.util.EnumSet;
048import java.util.Properties;
049import java.util.ResourceBundle;
050import java.util.Set;
051import java.util.concurrent.ExecutorService;
052import java.util.concurrent.Executors;
053import java.util.concurrent.ScheduledExecutorService;
054
055import org.apache.commons.cli.CommandLine;
056import org.apache.commons.cli.OptionBuilder;
057import org.apache.commons.cli.Options;
058import org.apache.commons.cli.ParseException;
059import org.dcm4che3.data.Tag;
060import org.dcm4che3.data.UID;
061import org.dcm4che3.data.Attributes;
062import org.dcm4che3.data.ElementDictionary;
063import org.dcm4che3.data.VR;
064import org.dcm4che3.io.DicomInputStream;
065import org.dcm4che3.io.DicomOutputStream;
066import org.dcm4che3.net.ApplicationEntity;
067import org.dcm4che3.net.Association;
068import org.dcm4che3.net.Connection;
069import org.dcm4che3.net.Device;
070import org.dcm4che3.net.DimseRSPHandler;
071import org.dcm4che3.net.IncompatibleConnectionException;
072import org.dcm4che3.net.PDVInputStream;
073import org.dcm4che3.net.QueryOption;
074import org.dcm4che3.net.Status;
075import org.dcm4che3.net.pdu.AAssociateRQ;
076import org.dcm4che3.net.pdu.ExtendedNegotiation;
077import org.dcm4che3.net.pdu.PresentationContext;
078import org.dcm4che3.net.pdu.RoleSelection;
079import org.dcm4che3.net.service.BasicCStoreSCP;
080import org.dcm4che3.net.service.DicomServiceException;
081import org.dcm4che3.net.service.DicomServiceRegistry;
082import org.dcm4che3.tool.common.CLIUtils;
083import org.dcm4che3.util.SafeClose;
084import org.dcm4che3.util.StringUtils;
085import org.slf4j.Logger;
086import org.slf4j.LoggerFactory;
087
088/**
089 * @author Gunter Zeilinger <gunterze@gmail.com>
090 *
091 */
092public class GetSCU {
093    
094    private static final Logger LOG = LoggerFactory.getLogger(GetSCU.class); 
095
096    public static enum InformationModel {
097        PatientRoot(UID.PatientRootQueryRetrieveInformationModelGET, "STUDY"),
098        StudyRoot(UID.StudyRootQueryRetrieveInformationModelGET, "STUDY"),
099        PatientStudyOnly(UID.PatientStudyOnlyQueryRetrieveInformationModelGETRetired, "STUDY"),
100        CompositeInstanceRoot(UID.CompositeInstanceRootRetrieveGET, "IMAGE"),
101        WithoutBulkData(UID.CompositeInstanceRetrieveWithoutBulkDataGET, "IMAGE"),
102        HangingProtocol(UID.HangingProtocolInformationModelGET, null),
103        ColorPalette(UID.ColorPaletteQueryRetrieveInformationModelGET, null);
104
105        private final String cuid;
106        final String level;
107
108        InformationModel(String cuid, String level) {
109            this.cuid = cuid;
110            this.level = level;
111       }
112
113        public String getCuid() {
114            return cuid;
115        }
116    }
117
118    private static ResourceBundle rb =
119        ResourceBundle.getBundle("org.dcm4che3.tool.getscu.messages");
120
121    private static final int[] DEF_IN_FILTER = {
122        Tag.SOPInstanceUID,
123        Tag.StudyInstanceUID,
124        Tag.SeriesInstanceUID
125    };
126
127    private Device device = new Device("getscu");
128    private final ApplicationEntity ae;
129    private final Connection conn = new Connection();
130    private final Connection remote = new Connection();
131    private final AAssociateRQ rq = new AAssociateRQ();
132    private int priority;
133    private InformationModel model;
134    private File storageDir;
135    private Attributes keys = new Attributes();
136    private int[] inFilter = DEF_IN_FILTER;
137    private Association as;
138
139    private BasicCStoreSCP storageSCP = new BasicCStoreSCP("*") {
140
141        @Override
142        protected void store(Association as, PresentationContext pc, Attributes rq,
143                PDVInputStream data, Attributes rsp)
144                throws IOException {
145            if (storageDir == null)
146                return;
147
148            String iuid = rq.getString(Tag.AffectedSOPInstanceUID);
149            String cuid = rq.getString(Tag.AffectedSOPClassUID);
150            String tsuid = pc.getTransferSyntax();
151            File file = new File(storageDir, iuid );
152            try {
153                storeTo(as, as.createFileMetaInformation(iuid, cuid, tsuid),
154                        data, file);
155            } catch (Exception e) {
156                throw new DicomServiceException(Status.ProcessingFailure, e);
157            }
158
159        }
160
161
162    };
163
164    public GetSCU() throws IOException {
165        ae = new ApplicationEntity("GETSCU");
166        device.addConnection(conn);
167        device.addApplicationEntity(ae);
168        ae.addConnection(conn);
169        device.setDimseRQHandler(createServiceRegistry());
170    }
171    
172    public GetSCU(ApplicationEntity appEntity) {
173        this.ae = appEntity;
174        this.device = this.ae.getDevice();
175    }
176
177    public ApplicationEntity getApplicationEntity() {
178        return ae;
179    }
180
181    public Connection getRemoteConnection() {
182        return remote;
183    }
184    
185    public AAssociateRQ getAAssociateRQ() {
186        return rq;
187    }
188    
189    public Association getAssociation() {
190        return as;
191    }
192
193    public Device getDevice() {
194        return device;
195    }    
196    
197    public Attributes getKeys() {
198        return keys;
199    }
200    
201    public static void storeTo(Association as, Attributes fmi, 
202            PDVInputStream data, File file) throws IOException  {
203        LOG.info("{}: M-WRITE {}", as, file);
204        file.getParentFile().mkdirs();
205        DicomOutputStream out = new DicomOutputStream(file);
206        try {
207            out.writeFileMetaInformation(fmi);
208            data.copyTo(out);
209        } finally {
210            SafeClose.close(out);
211        }
212    }
213
214    private DicomServiceRegistry createServiceRegistry() {
215        DicomServiceRegistry serviceRegistry = new DicomServiceRegistry();
216        serviceRegistry.addDicomService(storageSCP);
217        return serviceRegistry;
218    }
219
220    public void setStorageDirectory(File storageDir) {
221        if (storageDir != null)
222            if (storageDir.mkdirs())
223                System.out.println("M-WRITE " + storageDir);
224        this.storageDir = storageDir;
225    }
226
227    public final void setPriority(int priority) {
228        this.priority = priority;
229    }
230
231    public final void setInformationModel(InformationModel model, String[] tss,
232            boolean relational) {
233       this.model = model;
234       rq.addPresentationContext(new PresentationContext(1, model.getCuid(), tss));
235       if (relational)
236           rq.addExtendedNegotiation(new ExtendedNegotiation(model.getCuid(),
237                   QueryOption.toExtendedNegotiationInformation(EnumSet.of(QueryOption.RELATIONAL))));
238       if (model.level != null)
239           addLevel(model.level);
240    }
241
242    public void addLevel(String s) {
243        keys.setString(Tag.QueryRetrieveLevel, VR.CS, s);
244    }
245
246    public void addKey(int tag, String... ss) {
247        VR vr = ElementDictionary.vrOf(tag, keys.getPrivateCreator(tag));
248        keys.setString(tag, vr, ss);
249    }
250
251    public final void setInputFilter(int[] inFilter) {
252        this.inFilter  = inFilter;
253    }
254
255    private static CommandLine parseComandLine(String[] args)
256                throws ParseException {
257            Options opts = new Options();
258            addServiceClassOptions(opts);
259            addKeyOptions(opts);
260            addRetrieveLevelOption(opts);
261            addStorageDirectoryOptions(opts);
262            CLIUtils.addConnectOption(opts);
263            CLIUtils.addBindOption(opts, "GETSCU");
264            CLIUtils.addAEOptions(opts);
265            CLIUtils.addRetrieveTimeoutOption(opts);
266            CLIUtils.addPriorityOption(opts);
267            CLIUtils.addCommonOptions(opts);
268            return CLIUtils.parseComandLine(args, opts, rb, GetSCU.class);
269    }
270
271    @SuppressWarnings("static-access")
272    private static void addRetrieveLevelOption(Options opts) {
273        opts.addOption(OptionBuilder
274                .hasArg()
275                .withArgName("PATIENT|STUDY|SERIES|IMAGE|FRAME")
276                .withDescription(rb.getString("level"))
277                .create("L"));
278   }
279
280    @SuppressWarnings("static-access")
281    private static void addStorageDirectoryOptions(Options opts) {
282        opts.addOption(null, "ignore", false,
283                rb.getString("ignore"));
284        opts.addOption(OptionBuilder
285                .hasArg()
286                .withArgName("path")
287                .withDescription(rb.getString("directory"))
288                .withLongOpt("directory")
289                .create(null));
290    }
291
292    @SuppressWarnings("static-access")
293    private static void addKeyOptions(Options opts) {
294        opts.addOption(OptionBuilder
295                .hasArgs()
296                .withArgName("attr=value")
297                .withValueSeparator('=')
298                .withDescription(rb.getString("match"))
299                .create("m"));
300        opts.addOption(OptionBuilder
301                .hasArgs()
302                .withArgName("attr")
303                .withDescription(rb.getString("in-attr"))
304                .create("i"));
305    }
306
307    @SuppressWarnings("static-access")
308    private static void addServiceClassOptions(Options opts) {
309        opts.addOption(OptionBuilder
310                .hasArg()
311                .withArgName("name")
312                .withDescription(rb.getString("model"))
313                .create("M"));
314        opts.addOption(null, "relational", false, rb.getString("relational"));
315        CLIUtils.addTransferSyntaxOptions(opts);
316        opts.addOption(OptionBuilder
317                .hasArg()
318                .withArgName("cuid:tsuid[(,|;)...]")
319                .withDescription(rb.getString("store-tc"))
320                .withLongOpt("store-tc")
321                .create());
322        opts.addOption(OptionBuilder
323                .hasArg()
324                .withArgName("file|url")
325                .withDescription(rb.getString("store-tcs"))
326                .withLongOpt("store-tcs")
327                .create());
328    }
329
330
331    @SuppressWarnings("unchecked")
332    public static void main(String[] args) {
333        try {
334            CommandLine cl = parseComandLine(args);
335            GetSCU main = new GetSCU();
336            CLIUtils.configureConnect(main.remote, main.rq, cl);
337            CLIUtils.configureBind(main.conn, main.ae, cl);
338            CLIUtils.configure(main.conn, cl);
339            main.remote.setTlsProtocols(main.conn.tlsProtocols());
340            main.remote.setTlsCipherSuites(main.conn.getTlsCipherSuites());
341            configureServiceClass(main, cl);
342            configureKeys(main, cl);
343            main.setPriority(CLIUtils.priorityOf(cl));
344            configureStorageDirectory(main, cl);
345            ExecutorService executorService =
346                    Executors.newSingleThreadExecutor();
347            ScheduledExecutorService scheduledExecutorService =
348                    Executors.newSingleThreadScheduledExecutor();
349            main.device.setExecutor(executorService);
350            main.device.setScheduledExecutor(scheduledExecutorService);
351            try {
352                main.open();
353                List<String> argList = cl.getArgList();
354                if (argList.isEmpty())
355                    main.retrieve();
356                else
357                    for (String arg : argList)
358                        main.retrieve(new File(arg));
359            } finally {
360                main.close();
361                executorService.shutdown();
362                scheduledExecutorService.shutdown();
363            }
364       } catch (ParseException e) {
365            System.err.println("getscu: " + e.getMessage());
366            System.err.println(rb.getString("try"));
367            System.exit(2);
368        } catch (Exception e) {
369            System.err.println("getscu: " + e.getMessage());
370            e.printStackTrace();
371            System.exit(2);
372        }
373    }
374
375    private static void configureServiceClass(GetSCU main, CommandLine cl)
376            throws Exception {
377        main.setInformationModel(informationModelOf(cl),
378                CLIUtils.transferSyntaxesOf(cl), cl.hasOption("relational"));
379        String[] pcs = cl.getOptionValues("store-tc");
380        if (pcs != null)
381            for (String pc : pcs) {
382                String[] ss = StringUtils.split(pc, ':');
383                configureStorageSOPClass(main, ss[0], ss[1]);
384            }
385        String[] files = cl.getOptionValues("store-tcs");
386        if (pcs == null && files == null)
387            files = new String[] { "resource:store-tcs.properties" };
388        if (files != null)
389            for (String file : files) {
390                Properties p = CLIUtils.loadProperties(file, null);
391                Set<Entry<Object, Object>> entrySet = p.entrySet();
392                for (Entry<Object, Object> entry : entrySet)
393                    configureStorageSOPClass(main, (String) entry.getKey(), (String) entry.getValue());
394            }
395    }
396
397    private static void configureStorageSOPClass(GetSCU main, String cuid, String tsuids0) {
398        String[] tsuids1 = StringUtils.split(tsuids0, ';');
399        for (String tsuids2 : tsuids1) {
400            main.addOfferedStorageSOPClass(CLIUtils.toUID(cuid), CLIUtils.toUID(tsuids2));
401        }
402    }
403
404     public void addOfferedStorageSOPClass(String cuid, String... tsuids) {
405        if (!rq.containsPresentationContextFor(cuid))
406            rq.addRoleSelection(new RoleSelection(cuid, false, true));
407        rq.addPresentationContext(new PresentationContext(
408                2 * rq.getNumberOfPresentationContexts() + 1, cuid, tsuids));
409    }
410
411    private static void configureStorageDirectory(GetSCU main, CommandLine cl) {
412        if (!cl.hasOption("ignore")) {
413            main.setStorageDirectory(
414                    new File(cl.getOptionValue("directory", ".")));
415        }
416    }
417
418    private static void configureKeys(GetSCU main, CommandLine cl) {
419        if (cl.hasOption("m")) {
420            String[] keys = cl.getOptionValues("m");
421            for (int i = 1; i < keys.length; i++, i++)
422                main.addKey(CLIUtils.toTag(keys[i - 1]), StringUtils.split(keys[i], '/'));
423        }
424        if (cl.hasOption("L"))
425            main.addLevel(cl.getOptionValue("L"));
426        if (cl.hasOption("i"))
427            main.setInputFilter(CLIUtils.toTags(cl.getOptionValues("i")));
428    }
429
430    private static InformationModel informationModelOf(CommandLine cl) throws ParseException {
431        try {
432            return cl.hasOption("M")
433                    ? InformationModel.valueOf(cl.getOptionValue("M"))
434                    : InformationModel.StudyRoot;
435        } catch(IllegalArgumentException e) {
436            throw new ParseException(
437                    MessageFormat.format(
438                            rb.getString("invalid-model-name"),
439                            cl.getOptionValue("M")));
440        }
441    }
442
443    public void open() throws IOException, InterruptedException, IncompatibleConnectionException, GeneralSecurityException {
444        as = ae.connect(remote, rq);
445    }
446
447    public void close() throws IOException, InterruptedException {
448        if (as != null && as.isReadyForDataTransfer()) {
449            as.waitForOutstandingRSP();
450            as.release();
451        }
452    }
453
454    public void retrieve(File f) throws IOException, InterruptedException {
455        Attributes attrs = new Attributes();
456        DicomInputStream dis = null;
457        try {
458            dis = new DicomInputStream(f);
459            attrs.addSelected(dis.readDataset(-1, -1), inFilter);
460        } finally {
461            SafeClose.close(dis);
462        }
463        attrs.addAll(keys);
464        retrieve(attrs);
465    }
466
467    public void retrieve() throws IOException, InterruptedException {
468        retrieve(keys);
469    }
470    
471    private void retrieve(Attributes keys) throws IOException, InterruptedException {
472         DimseRSPHandler rspHandler = new DimseRSPHandler(as.nextMessageID()) {
473
474            @Override
475            public void onDimseRSP(Association as, Attributes cmd,
476                    Attributes data) {
477                super.onDimseRSP(as, cmd, data);
478            }
479        };
480
481        retrieve (keys, rspHandler);
482    }
483    
484    public void retrieve(DimseRSPHandler rspHandler) throws IOException, InterruptedException {
485        retrieve(keys, rspHandler);
486    }
487    
488    private void retrieve(Attributes keys, DimseRSPHandler rspHandler) throws IOException, InterruptedException {
489        as.cget(model.getCuid(), priority, keys, null, rspHandler);
490    }
491
492}