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.movescu;
040
041import java.io.File;
042import java.io.IOException;
043import java.security.GeneralSecurityException;
044import java.text.MessageFormat;
045import java.util.EnumSet;
046import java.util.List;
047import java.util.ResourceBundle;
048import java.util.concurrent.ExecutorService;
049import java.util.concurrent.Executors;
050import java.util.concurrent.ScheduledExecutorService;
051
052import org.apache.commons.cli.CommandLine;
053import org.apache.commons.cli.OptionBuilder;
054import org.apache.commons.cli.Options;
055import org.apache.commons.cli.ParseException;
056import org.dcm4che3.data.Tag;
057import org.dcm4che3.data.UID;
058import org.dcm4che3.data.Attributes;
059import org.dcm4che3.data.ElementDictionary;
060import org.dcm4che3.data.VR;
061import org.dcm4che3.io.DicomInputStream;
062import org.dcm4che3.net.ApplicationEntity;
063import org.dcm4che3.net.Association;
064import org.dcm4che3.net.Connection;
065import org.dcm4che3.net.Device;
066import org.dcm4che3.net.DimseRSPHandler;
067import org.dcm4che3.net.IncompatibleConnectionException;
068import org.dcm4che3.net.QueryOption;
069import org.dcm4che3.net.Status;
070import org.dcm4che3.net.pdu.AAssociateRQ;
071import org.dcm4che3.net.pdu.ExtendedNegotiation;
072import org.dcm4che3.net.pdu.PresentationContext;
073import org.dcm4che3.tool.common.CLIUtils;
074import org.dcm4che3.util.SafeClose;
075import org.dcm4che3.util.StringUtils;
076import org.slf4j.Logger;
077import org.slf4j.LoggerFactory;
078
079/**
080 * @author Gunter Zeilinger <gunterze@gmail.com>
081 *
082 */
083public class MoveSCU {
084
085    public static enum InformationModel {
086        PatientRoot(UID.PatientRootQueryRetrieveInformationModelMOVE, "STUDY"),
087        StudyRoot(UID.StudyRootQueryRetrieveInformationModelMOVE, "STUDY"),
088        PatientStudyOnly(UID.PatientStudyOnlyQueryRetrieveInformationModelMOVERetired, "STUDY"),
089        CompositeInstanceRoot(UID.CompositeInstanceRootRetrieveMOVE, "IMAGE"),
090        HangingProtocol(UID.HangingProtocolInformationModelMOVE, null),
091        ColorPalette(UID.ColorPaletteQueryRetrieveInformationModelMOVE, null);
092
093        final String cuid;
094        final String level;
095
096        InformationModel(String cuid, String level) {
097            this.cuid = cuid;
098            this.level = level;
099       }
100        public String getCuid() {
101            return cuid;
102        }
103
104    }
105
106    private static ResourceBundle rb =
107        ResourceBundle.getBundle("org.dcm4che3.tool.movescu.messages");
108
109    private static final int[] DEF_IN_FILTER = {
110        Tag.SOPInstanceUID,
111        Tag.StudyInstanceUID,
112        Tag.SeriesInstanceUID
113    };
114
115    private static final Logger LOG = LoggerFactory.getLogger(MoveSCU.class);
116    
117    private ApplicationEntity ae = new ApplicationEntity("MOVESCU");
118    private final Connection conn = new Connection();
119    private final Connection remote = new Connection();
120    private final AAssociateRQ rq = new AAssociateRQ();
121    private Device device;
122    private int priority;
123    private String destination;
124    private InformationModel model;
125    private Attributes keys = new Attributes();
126    private int[] inFilter = DEF_IN_FILTER;
127    private Association as;
128    private int idleRetrieveTimeout;
129    private int exitCode;
130
131    public MoveSCU() throws IOException {
132        this.device = new Device("movescu");
133        this.device.addConnection(conn);
134        this.device.addApplicationEntity(ae);
135        ae.addConnection(conn);
136    }
137
138    public MoveSCU(ApplicationEntity ae) {
139        this.ae = ae;
140        this.device = ae.getDevice();
141    }
142
143    public final void setPriority(int priority) {
144        this.priority = priority;
145    }
146
147    public final void setInformationModel(InformationModel model, String[] tss,
148            boolean relational) {
149       this.model = model;
150       rq.addPresentationContext(new PresentationContext(1, model.cuid, tss));
151       if (relational)
152           rq.addExtendedNegotiation(new ExtendedNegotiation(model.cuid, 
153                   QueryOption.toExtendedNegotiationInformation(EnumSet.of(QueryOption.RELATIONAL))));
154       if (model.level != null)
155           addLevel(model.level);
156    }
157
158    public ApplicationEntity getApplicationEntity() {
159        return ae;
160    }
161
162    public Connection getRemoteConnection() {
163        return remote;
164    }
165    
166    public AAssociateRQ getAAssociateRQ() {
167        return rq;
168    }
169    
170    public Association getAssociation() {
171        return as;
172    }
173
174    public Device getDevice() {
175        return device;
176    }    
177    
178    public Attributes getKeys() {
179        return keys;
180    }
181    public void addLevel(String s) {
182        keys.setString(Tag.QueryRetrieveLevel, VR.CS, s);
183    }
184
185    public final void setDestination(String destination) {
186        this.destination = destination;
187    }
188
189    public void addKey(int tag, String... ss) {
190        VR vr = ElementDictionary.vrOf(tag, keys.getPrivateCreator(tag));
191        keys.setString(tag, vr, ss);
192    }
193
194    public final void setInputFilter(int[] inFilter) {
195        this.inFilter  = inFilter;
196    }
197
198    private static CommandLine parseComandLine(String[] args)
199                throws ParseException {
200            Options opts = new Options();
201            addServiceClassOptions(opts);
202            addKeyOptions(opts);
203            addRetrieveLevelOption(opts);
204            addDestinationOption(opts);
205            addTimeoutOption(opts);
206            CLIUtils.addConnectOption(opts);
207            CLIUtils.addBindOption(opts, "MOVESCU");
208            CLIUtils.addAEOptions(opts);
209            CLIUtils.addRetrieveTimeoutOption(opts);
210            CLIUtils.addPriorityOption(opts);
211            CLIUtils.addCommonOptions(opts);
212            return CLIUtils.parseComandLine(args, opts, rb, MoveSCU.class);
213    }
214
215    @SuppressWarnings("static-access")
216    private static void addRetrieveLevelOption(Options opts) {
217        opts.addOption(OptionBuilder
218                .hasArg()
219                .withArgName("PATIENT|STUDY|SERIES|IMAGE|FRAME")
220                .withDescription(rb.getString("level"))
221                .create("L"));
222   }
223
224    @SuppressWarnings("static-access")
225    private static void addDestinationOption(Options opts) {
226        opts.addOption(OptionBuilder
227                .withLongOpt("dest")
228                .hasArg()
229                .withArgName("aet")
230                .withDescription(rb.getString("dest"))
231                .create());
232        
233    }
234
235    @SuppressWarnings("static-access")
236    private static void addTimeoutOption(Options opts) {
237        opts.addOption(OptionBuilder
238                .hasArg()
239                .withArgName("ms")
240                .withDescription(rb.getString("idle-retrieve-timeout"))
241                .withLongOpt("idle-retrieve-timeout")
242                .create(null));
243    }
244
245    @SuppressWarnings("static-access")
246    private static void addKeyOptions(Options opts) {
247        opts.addOption(OptionBuilder
248                .hasArgs()
249                .withArgName("attr=value")
250                .withValueSeparator('=')
251                .withDescription(rb.getString("match"))
252                .create("m"));
253        opts.addOption(OptionBuilder
254                .hasArgs()
255                .withArgName("attr")
256                .withDescription(rb.getString("in-attr"))
257                .create("i"));
258    }
259
260    @SuppressWarnings("static-access")
261    private static void addServiceClassOptions(Options opts) {
262        opts.addOption(OptionBuilder
263                .hasArg()
264                .withArgName("name")
265                .withDescription(rb.getString("model"))
266                .create("M"));
267        CLIUtils.addTransferSyntaxOptions(opts);
268        opts.addOption(null, "relational", false, rb.getString("relational"));
269    }
270
271    @SuppressWarnings("unchecked")
272    public static void main(String[] args) {
273        try {
274            CommandLine cl = parseComandLine(args);
275            MoveSCU main = new MoveSCU();
276            CLIUtils.configureConnect(main.remote, main.rq, cl);
277            CLIUtils.configureBind(main.conn, main.ae, cl);
278            CLIUtils.configure(main.conn, cl);
279            main.remote.setTlsProtocols(main.conn.tlsProtocols());
280            main.remote.setTlsCipherSuites(main.conn.getTlsCipherSuites());
281            configureServiceClass(main, cl);
282            configureKeys(main, cl);
283            main.setPriority(CLIUtils.priorityOf(cl));
284            main.setDestination(destinationOf(cl));
285            main.idleRetrieveTimeout = CLIUtils.getIntOption(cl, "idle-retrieve-timeout", -1);
286            ExecutorService executorService =
287                    Executors.newSingleThreadExecutor();
288            ScheduledExecutorService scheduledExecutorService =
289                    Executors.newSingleThreadScheduledExecutor();
290            main.device.setExecutor(executorService);
291            main.device.setScheduledExecutor(scheduledExecutorService);
292            try {
293                main.open();
294                List<String> argList = cl.getArgList();
295                if (argList.isEmpty())
296                    main.retrieve();
297                else
298                    for (String arg : argList)
299                        main.retrieve(new File(arg));
300            } finally {
301                main.close();
302                executorService.shutdown();
303                scheduledExecutorService.shutdown();
304            }
305            System.exit(main.exitCode);
306       } catch (ParseException e) {
307            System.err.println("movescu: " + e.getMessage());
308            System.err.println(rb.getString("try"));
309            System.exit(2);
310        } catch (Exception e) {
311            System.err.println("movescu: " + e.getMessage());
312            e.printStackTrace();
313            System.exit(2);
314        }
315    }
316
317    private static void configureServiceClass(MoveSCU main, CommandLine cl) throws ParseException {
318        main.setInformationModel(informationModelOf(cl),
319                CLIUtils.transferSyntaxesOf(cl), cl.hasOption("relational"));
320    }
321
322    private static String destinationOf(CommandLine cl) throws ParseException {
323        if (cl.hasOption("dest"))
324            return cl.getOptionValue("dest");
325        throw new ParseException(rb.getString("missing-dest"));
326    }
327
328    private static void configureKeys(MoveSCU main, CommandLine cl) {
329        if (cl.hasOption("m")) {
330            String[] keys = cl.getOptionValues("m");
331            for (int i = 1; i < keys.length; i++, i++)
332                main.addKey(CLIUtils.toTag(keys[i - 1]), StringUtils.split(keys[i], '/'));
333        }
334        if (cl.hasOption("L"))
335            main.addLevel(cl.getOptionValue("L"));
336        if (cl.hasOption("i"))
337            main.setInputFilter(CLIUtils.toTags(cl.getOptionValues("i")));
338    }
339
340    private static InformationModel informationModelOf(CommandLine cl) throws ParseException {
341        try {
342            return cl.hasOption("M")
343                    ? InformationModel.valueOf(cl.getOptionValue("M"))
344                    : InformationModel.StudyRoot;
345        } catch(IllegalArgumentException e) {
346            throw new ParseException(MessageFormat.format(
347                    rb.getString("invalid-model-name"),
348                    cl.getOptionValue("M")));
349        }
350    }
351
352    public void open() throws IOException, InterruptedException,
353            IncompatibleConnectionException, GeneralSecurityException {
354        as = ae.connect(remote, rq);
355    }
356
357    public void close() throws IOException, InterruptedException {
358        if (as != null && as.isReadyForDataTransfer()) {
359            as.waitForOutstandingRSP();
360            as.release();
361        }
362    }
363
364    public void retrieve(File f) throws IOException, InterruptedException {
365        Attributes attrs = new Attributes();
366        DicomInputStream dis = null;
367        try {
368            attrs.addSelected(new DicomInputStream(f).readDataset(-1, -1), inFilter);
369        } finally {
370            SafeClose.close(dis);
371        }
372        attrs.addAll(keys);
373        retrieve(attrs);
374    }
375
376    public void retrieve() throws IOException, InterruptedException {
377        retrieve(keys);
378    }
379
380    private void retrieve(Attributes keys) throws IOException, InterruptedException {
381         DimseRSPHandler rspHandler = new DimseRSPHandler(as.nextMessageID()) {
382            int lastRemaining = -1;
383            long lastChanged;
384            @Override
385            public void onDimseRSP(Association as, Attributes cmd,
386                    Attributes data) {
387                super.onDimseRSP(as, cmd, data);
388                if (idleRetrieveTimeout != -1 && Status.isPending(cmd.getInt(Tag.Status, -1))) {
389                    int remaining = cmd.getInt(Tag.NumberOfRemainingSuboperations, -1);
390                    if(remaining > 0) {
391                        if(lastRemaining != remaining) {
392                            lastRemaining = remaining;
393                            lastChanged = System.currentTimeMillis();
394                        } else {
395                            long idleTime = System.currentTimeMillis()-lastChanged;
396                            if (idleTime > idleRetrieveTimeout){
397                                LOG.warn("Cancel C-MOVE request after "+idleTime+"ms of idle time! response:"+cmd);
398                                try {
399                                    exitCode = 3;
400                                    cancel(as);
401                                } catch (IOException e) {
402                                    e.printStackTrace();
403                                }
404                            } else {
405                                LOG.info("C_MOVE Request is idle for "+idleTime+"ms! idleRetrieveTimeout="+idleRetrieveTimeout);
406                            }
407                        }
408                    }
409                }
410            }
411        };
412
413        as.cmove(model.cuid, priority, keys, null, destination, rspHandler);
414    }
415
416    public void retrieve(Attributes keys, DimseRSPHandler handler) throws IOException, InterruptedException {
417       as.cmove(model.cuid, priority, keys, null, destination, handler);
418   }
419
420    public void setLevel(InformationModel mdl) {
421        this.model = mdl;
422        if(mdl.level.equalsIgnoreCase("IMAGE")) {
423            this.rq.addExtendedNegotiation(new ExtendedNegotiation(model.cuid, new byte[]{1}));
424        }
425    }
426}