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.stgcmtscu;
040
041import java.io.File;
042import java.io.IOException;
043import java.security.GeneralSecurityException;
044import java.text.MessageFormat;
045import java.util.ArrayList;
046import java.util.HashMap;
047import java.util.HashSet;
048import java.util.List;
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.OptionBuilder;
056import org.apache.commons.cli.Options;
057import org.apache.commons.cli.ParseException;
058import org.dcm4che3.data.Tag;
059import org.dcm4che3.data.UID;
060import org.dcm4che3.data.Attributes;
061import org.dcm4che3.data.Sequence;
062import org.dcm4che3.data.VR;
063import org.dcm4che3.io.DicomOutputStream;
064import org.dcm4che3.net.ApplicationEntity;
065import org.dcm4che3.net.Association;
066import org.dcm4che3.net.AssociationStateException;
067import org.dcm4che3.net.Commands;
068import org.dcm4che3.net.Connection;
069import org.dcm4che3.net.Device;
070import org.dcm4che3.net.Dimse;
071import org.dcm4che3.net.DimseRSPHandler;
072import org.dcm4che3.net.IncompatibleConnectionException;
073import org.dcm4che3.net.Status;
074import org.dcm4che3.net.TransferCapability;
075import org.dcm4che3.net.pdu.AAssociateRQ;
076import org.dcm4che3.net.pdu.PresentationContext;
077import org.dcm4che3.net.service.AbstractDicomService;
078import org.dcm4che3.net.service.BasicCEchoSCP;
079import org.dcm4che3.net.service.DicomService;
080import org.dcm4che3.net.service.DicomServiceException;
081import org.dcm4che3.net.service.DicomServiceRegistry;
082import org.dcm4che3.tool.common.CLIUtils;
083import org.dcm4che3.tool.common.DicomFiles;
084import org.dcm4che3.util.SafeClose;
085import org.dcm4che3.util.UIDUtils;
086import org.slf4j.Logger;
087import org.slf4j.LoggerFactory;
088
089/**
090 * @author Gunter Zeilinger <gunterze@gmail.com>
091 * @author Michael Backhaus <michael.backhaus@agfa.com>
092 */
093public class StgCmtSCU {
094
095    private static ResourceBundle rb =
096            ResourceBundle.getBundle("org.dcm4che3.tool.stgcmtscu.messages");
097    
098    private static final Logger LOG = LoggerFactory.getLogger(StgCmtSCU.class);
099
100    private final ApplicationEntity ae;
101    private final Connection remote;
102    private final AAssociateRQ rq = new AAssociateRQ();
103    private Attributes attrs;
104    private String uidSuffix;
105    private File storageDir;
106    private boolean keepAlive;
107    private int splitTag;
108    private int status;
109    private HashMap<String,List<String>> map = new HashMap<String,List<String>>();
110    private Association as;
111
112    private final HashSet<String> outstandingResults = new HashSet<String>(2);
113    private final DicomService stgcmtResultHandler =
114            new AbstractDicomService(UID.StorageCommitmentPushModelSOPClass) {
115
116             @Override
117             public void onDimseRQ(Association as, PresentationContext pc,
118                     Dimse dimse, Attributes cmd, Attributes data)
119                     throws IOException {
120                 if (dimse != Dimse.N_EVENT_REPORT_RQ)
121                     throw new DicomServiceException(Status.UnrecognizedOperation);
122
123                 int eventTypeID = cmd.getInt(Tag.EventTypeID, 0);
124                 if (eventTypeID != 1 && eventTypeID != 2) 
125                     throw new DicomServiceException(Status.NoSuchEventType)
126                                 .setEventTypeID(eventTypeID);
127                 String tuid = data.getString(Tag.TransactionUID);
128                 try {
129                     Attributes rsp = Commands.mkNEventReportRSP(cmd, status);
130                     Attributes rspAttrs = StgCmtSCU.this.eventRecord(as, cmd, data);
131                     as.writeDimseRSP(pc, rsp, rspAttrs);
132                     removeOutstandingResult(tuid);
133                 } catch (AssociationStateException e) {
134                     LOG.warn("{} << N-EVENT-RECORD-RSP failed: {}", as, e.getMessage());
135                 }
136             }
137    };
138
139    public StgCmtSCU(ApplicationEntity ae) throws IOException {
140        this.remote = new Connection();
141        this.ae = ae;
142        DicomServiceRegistry serviceRegistry = new DicomServiceRegistry();
143        serviceRegistry.addDicomService(new BasicCEchoSCP());
144        serviceRegistry.addDicomService(stgcmtResultHandler);
145        ae.setDimseRQHandler(serviceRegistry);
146    }
147
148    public StgCmtSCU(ApplicationEntity ae, DicomService stgCmtResultHndlr ) throws IOException {
149        this.remote = new Connection();
150        this.ae = ae;
151        DicomServiceRegistry serviceRegistry = new DicomServiceRegistry();
152        serviceRegistry.addDicomService(new BasicCEchoSCP());
153        serviceRegistry.addDicomService(stgCmtResultHndlr);
154        ae.setDimseRQHandler(serviceRegistry);
155    }
156
157    public Connection getRemoteConnection() {
158        return remote;
159    }
160
161    public AAssociateRQ getAAssociateRQ() {
162        return rq;
163    }
164
165    public void setStorageDirectory(File storageDir) {
166        if (storageDir != null)
167            storageDir.mkdirs();
168        this.storageDir = storageDir;
169    }
170
171    public File getStorageDirectory() {
172        return storageDir;
173    }
174
175    public final void setUIDSuffix(String uidSuffix) {
176        this.uidSuffix = uidSuffix;
177    }
178
179    public void setAttributes(Attributes attrs) {
180        this.attrs = attrs;
181    }
182
183    @SuppressWarnings("unchecked")
184    public static void main(String[] args) {
185        try {
186            CommandLine cl = parseComandLine(args);
187            Device device = new Device("stgcmtscu");
188            Connection conn = new Connection();
189            device.addConnection(conn);
190            ApplicationEntity ae = new ApplicationEntity("STGCMTSCU");
191            device.addApplicationEntity(ae);
192            ae.addConnection(conn);
193            final StgCmtSCU stgcmtscu = new StgCmtSCU(ae);
194            CLIUtils.configureConnect(stgcmtscu.remote, stgcmtscu.rq, cl);
195            CLIUtils.configureBind(conn, stgcmtscu.ae, cl);
196            CLIUtils.configure(conn, cl);
197            stgcmtscu.remote.setTlsProtocols(conn.tlsProtocols());
198            stgcmtscu.remote.setTlsCipherSuites(conn.getTlsCipherSuites());
199            stgcmtscu.setTransferSyntaxes(CLIUtils.transferSyntaxesOf(cl));
200            stgcmtscu.setStatus(CLIUtils.getIntOption(cl, "status", 0));
201            stgcmtscu.setSplitTag(getSplitTag(cl));
202            stgcmtscu.setKeepAlive(cl.hasOption("keep-alive"));
203            stgcmtscu.setStorageDirectory(getStorageDirectory(cl));
204            stgcmtscu.setAttributes(new Attributes());
205            CLIUtils.addAttributes(stgcmtscu.attrs, cl.getOptionValues("s"));
206            stgcmtscu.setUIDSuffix(cl.getOptionValue("uid-suffix"));
207            List<String> argList = cl.getArgList();
208            boolean echo = argList.isEmpty();
209            if (!echo) {
210                LOG.info(rb.getString("scanning"));
211                DicomFiles.scan(argList, new DicomFiles.Callback() {
212                    
213                    @Override
214                    public boolean dicomFile(File f, Attributes fmi, long dsPos,
215                            Attributes ds) {
216                        return stgcmtscu.addInstance(ds);
217                    }
218                });
219            }
220            ExecutorService executorService =
221                    Executors.newCachedThreadPool();
222            ScheduledExecutorService scheduledExecutorService =
223                    Executors.newSingleThreadScheduledExecutor();
224            device.setExecutor(executorService);
225            device.setScheduledExecutor(scheduledExecutorService);
226            device.bindConnections();
227            try {
228                stgcmtscu.open();
229                if (echo)
230                    stgcmtscu.echo();
231                else
232                    stgcmtscu.sendRequests();
233             } finally {
234                stgcmtscu.close();
235                if (conn.isListening()) {
236                    device.waitForNoOpenConnections();
237                    device.unbindConnections();
238                }
239                executorService.shutdown();
240                scheduledExecutorService.shutdown();
241            }
242        } catch (ParseException e) {
243            System.err.println("stgcmtscu: " + e.getMessage());
244            System.err.println(rb.getString("try"));
245            System.exit(2);
246        } catch (Exception e) {
247            System.err.println("stgcmtscu: " + e.getMessage());
248            e.printStackTrace();
249            System.exit(2);
250        }
251    }
252
253    public static File getStorageDirectory(CommandLine cl) {
254        return cl.hasOption("ignore")
255                ? null
256                : new File(cl.getOptionValue("directory", "."));
257    }
258
259    public static int getSplitTag(CommandLine cl) {
260        return cl.hasOption("one-by-study") 
261                ? Tag.StudyInstanceUID
262                : cl.hasOption("one-by-series")
263                        ? Tag.SeriesInstanceUID
264                        : 0;
265    }
266
267    public void setSplitTag(int splitTag) {
268        this.splitTag = splitTag;
269    }
270
271    public void setKeepAlive(boolean keepAlive) {
272        this.keepAlive = keepAlive;
273    }
274
275    public void setStatus(int status) {
276        this.status = status;
277    }
278
279    public void setTransferSyntaxes(String[] tss) {
280        rq.addPresentationContext(
281                new PresentationContext(1, UID.VerificationSOPClass,
282                        UID.ImplicitVRLittleEndian));
283        rq.addPresentationContext(
284                new PresentationContext(2,
285                        UID.StorageCommitmentPushModelSOPClass,
286                        tss));
287        ae.addTransferCapability(
288                new TransferCapability(null,
289                        UID.VerificationSOPClass,
290                        TransferCapability.Role.SCP,
291                        UID.ImplicitVRLittleEndian));
292        ae.addTransferCapability(
293                new TransferCapability(null,
294                        UID.StorageCommitmentPushModelSOPClass,
295                        TransferCapability.Role.SCU,
296                        tss));
297    }
298
299    public boolean addInstance(Attributes inst) {
300        CLIUtils.updateAttributes(inst, attrs, uidSuffix);
301        String cuid = inst.getString(Tag.SOPClassUID);
302        String iuid = inst.getString(Tag.SOPInstanceUID);
303        String splitkey = splitTag != 0 ? inst.getString(splitTag) : "";
304        if (cuid == null || iuid == null || splitkey == null)
305            return false;
306
307        List<String> refSOPs = map.get(splitkey);
308        if (refSOPs == null)
309            map.put(splitkey, refSOPs = new ArrayList<String>());
310
311        refSOPs.add(cuid);
312        refSOPs.add(iuid);
313        return true;
314    }
315
316    private static CommandLine parseComandLine(String[] args)
317            throws ParseException{
318        Options opts = new Options();
319        CLIUtils.addTransferSyntaxOptions(opts);
320        CLIUtils.addConnectOption(opts);
321        CLIUtils.addBindOption(opts, "STGCMTSCU");
322        CLIUtils.addRequestTimeoutOption(opts);
323        CLIUtils.addAEOptions(opts);
324        CLIUtils.addResponseTimeoutOption(opts);
325        CLIUtils.addCommonOptions(opts);
326        addStgCmtOptions(opts);
327        return CLIUtils.parseComandLine(args, opts, rb, StgCmtSCU.class);
328    }
329
330    @SuppressWarnings("static-access")
331    public static void addStgCmtOptions(Options opts) {
332        opts.addOption(null, "ignore", false,
333                rb.getString("ignore"));
334        opts.addOption(OptionBuilder
335                .hasArg()
336                .withArgName("path")
337                .withDescription(rb.getString("directory"))
338                .withLongOpt("directory")
339                .create(null));
340        opts.addOption(OptionBuilder
341                .hasArg()
342                .withArgName("code")
343                .withDescription(rb.getString("status"))
344                .withLongOpt("status")
345                .create(null));
346        opts.addOption(null, "keep-alive", false, rb.getString("keep-alive"));
347        opts.addOption(null, "one-per-study", false, rb.getString("one-per-study"));
348        opts.addOption(null, "one-per-series", false, rb.getString("one-per-series"));
349        opts.addOption(OptionBuilder
350                .hasArgs()
351                .withArgName("[seq/]attr=value")
352                .withValueSeparator('=')
353                .withDescription(rb.getString("set"))
354                .create("s"));
355        opts.addOption(OptionBuilder
356                .hasArg()
357                .withArgName("suffix")
358                .withDescription(rb.getString("uid-suffix"))
359                .withLongOpt("uid-suffix")
360                .create(null));
361    }
362
363    public void open() throws IOException, InterruptedException,
364            IncompatibleConnectionException, GeneralSecurityException {
365        as = ae.connect(remote, rq);
366    }
367
368    public void echo() throws IOException, InterruptedException {
369        as.cecho().next();
370    }
371
372    public void close() throws IOException, InterruptedException {
373        if (as != null) {
374            if (as.isReadyForDataTransfer()) {
375                as.waitForOutstandingRSP();
376                if (keepAlive)
377                    waitForOutstandingResults(as);
378                as.release();
379            }
380            as.waitForSocketClose();
381        }
382        waitForOutstandingResults(as);
383    }
384
385    public void addOutstandingResult(String tuid) {
386        synchronized (outstandingResults ) {
387            outstandingResults.add(tuid);
388        }
389    }
390
391    public void removeOutstandingResult(String tuid) {
392        synchronized (outstandingResults ) {
393            outstandingResults.remove(tuid);
394            outstandingResults.notify();
395        }
396    }
397
398    private void waitForOutstandingResults(Association as) throws InterruptedException {
399        synchronized (outstandingResults) {
400
401            int requestTimeout = as.getConnection().getRequestTimeout();
402            long started = System.currentTimeMillis();
403
404            int lastSize = -1;
405            while (!outstandingResults.isEmpty()) {
406                if (outstandingResults.size() != lastSize) {
407                    lastSize = outstandingResults.size();
408                    System.out.println(MessageFormat.format(rb.getString("wait-for-results"),outstandingResults.size()));
409                }
410
411                outstandingResults.wait(100);
412
413                // timeout 10 sec
414                if (requestTimeout > 0 && System.currentTimeMillis() - started > requestTimeout)
415                    throw new RuntimeException("Timeout (10 sec) while waiting for storage commitment");
416            }
417        }
418    }
419
420    public Attributes makeActionInfo(List<String> refSOPs) {
421        Attributes actionInfo = new Attributes(2);
422        actionInfo.setString(Tag.TransactionUID, VR.UI, UIDUtils.createUID());
423        int n = refSOPs.size() / 2;
424        Sequence refSOPSeq = actionInfo.newSequence(Tag.ReferencedSOPSequence, n);
425        for (int i = 0, j = 0; j < n; j++) {
426            Attributes refSOP = new Attributes(2);
427            refSOP.setString(Tag.ReferencedSOPClassUID, VR.UI, refSOPs.get(i++));
428            refSOP.setString(Tag.ReferencedSOPInstanceUID, VR.UI, refSOPs.get(i++));
429            refSOPSeq.add(refSOP);
430        }
431        return actionInfo;
432    }
433
434    public void sendRequests() throws IOException, InterruptedException {
435        for (List<String> refSOPs : map.values())
436            sendRequest(makeActionInfo(refSOPs));
437    }
438
439    private void sendRequest(Attributes actionInfo) throws IOException, InterruptedException {
440        final String tuid = actionInfo.getString(Tag.TransactionUID);
441        DimseRSPHandler rspHandler = new DimseRSPHandler(as.nextMessageID()) {
442
443            @Override
444            public void onDimseRSP(Association as, Attributes cmd, Attributes data) {
445                if (cmd.getInt(Tag.Status, -1) != Status.Success)
446                    removeOutstandingResult(tuid );
447                super.onDimseRSP(as, cmd, data);
448            }
449        };
450
451        as.naction(UID.StorageCommitmentPushModelSOPClass,
452                UID.StorageCommitmentPushModelSOPInstance,
453                1, actionInfo, null, rspHandler);
454        addOutstandingResult(tuid);
455    }
456
457    private Attributes eventRecord(Association as, Attributes cmd, Attributes eventInfo)
458            throws DicomServiceException {
459        if (storageDir == null)
460            return null;
461
462        String cuid = cmd.getString(Tag.AffectedSOPClassUID);
463        String iuid = cmd.getString(Tag.AffectedSOPInstanceUID);
464        String tuid = eventInfo.getString(Tag.TransactionUID);
465        File file = new File(storageDir, tuid );
466        DicomOutputStream out = null;
467        LOG.info("{}: M-WRITE {}", as, file);
468        try {
469            out = new DicomOutputStream(file);
470            out.writeDataset(
471                    Attributes.createFileMetaInformation(iuid, cuid,
472                            UID.ExplicitVRLittleEndian),
473                    eventInfo);
474        } catch (IOException e) {
475            LOG.warn(as + ": Failed to store Storage Commitment Result:", e);
476            throw new DicomServiceException(Status.ProcessingFailure, e);
477        } finally {
478            SafeClose.close(out);
479        }
480        return null;
481    }
482}