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) 2012
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.ihe.modality;
040
041import java.io.BufferedReader;
042import java.io.BufferedWriter;
043import java.io.File;
044import java.io.FileOutputStream;
045import java.io.IOException;
046import java.io.InputStreamReader;
047import java.io.OutputStreamWriter;
048import java.security.GeneralSecurityException;
049import java.text.MessageFormat;
050import java.util.Arrays;
051import java.util.List;
052import java.util.ResourceBundle;
053import java.util.concurrent.ExecutorService;
054import java.util.concurrent.Executors;
055import java.util.concurrent.ScheduledExecutorService;
056
057import org.apache.commons.cli.CommandLine;
058import org.apache.commons.cli.MissingOptionException;
059import org.apache.commons.cli.OptionBuilder;
060import org.apache.commons.cli.OptionGroup;
061import org.apache.commons.cli.Options;
062import org.apache.commons.cli.ParseException;
063import org.dcm4che3.data.Tag;
064import org.dcm4che3.data.UID;
065import org.dcm4che3.data.Attributes;
066import org.dcm4che3.data.Sequence;
067import org.dcm4che3.data.VR;
068import org.dcm4che3.net.ApplicationEntity;
069import org.dcm4che3.net.Connection;
070import org.dcm4che3.net.Device;
071import org.dcm4che3.net.IncompatibleConnectionException;
072import org.dcm4che3.tool.common.CLIUtils;
073import org.dcm4che3.tool.common.DicomFiles;
074import org.dcm4che3.tool.mkkos.MkKOS;
075import org.dcm4che3.tool.mppsscu.MppsSCU;
076import org.dcm4che3.tool.stgcmtscu.StgCmtSCU;
077import org.dcm4che3.tool.storescu.StoreSCU;
078import org.dcm4che3.util.UIDUtils;
079
080/**
081 * @author Michael Backhaus <michael.backhaus@agfa.com>
082 * @author Gunter Zeilinger <gunterze@gmail.com>
083 */
084public class Modality {
085    
086    private static ResourceBundle rb =
087            ResourceBundle.getBundle("org.dcm4che3.tool.ihe.modality.messages");
088    
089    static BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
090    
091    private static String calledAET;
092    
093    @SuppressWarnings({ "unchecked" })
094    public static void main(String[] args) {
095        try {
096            CommandLine cl = parseComandLine(args);
097            if(cl.getArgList().isEmpty())
098                throw new MissingOptionException(
099                        rb.getString("missing-i-file"));
100            final Device device = new Device("modality");
101            final Connection conn = new Connection();
102            final ApplicationEntity ae = new ApplicationEntity("MODALITY");
103            checkOptions(cl);
104            CLIUtils.configureBind(conn, ae, cl);
105            CLIUtils.configure(conn, cl);
106            device.addConnection(conn);
107            device.addApplicationEntity(ae);
108            ae.addConnection(conn);
109            final MppsSCU mppsscu = new MppsSCU(ae);
110            final StoreSCU storescu = new StoreSCU(ae);
111            final StgCmtSCU stgcmtscu = new StgCmtSCU(ae);
112            CLIUtils.configureConnect(mppsscu.getRemoteConnection(), mppsscu.getAAssociateRQ(), cl);
113            CLIUtils.configureConnect(stgcmtscu.getRemoteConnection(), stgcmtscu.getAAssociateRQ(), cl);
114            CLIUtils.configureConnect(storescu.getRemoteConnection(), storescu.getAAssociateRQ(), cl);
115            calledAET = storescu.getAAssociateRQ().getCalledAET();
116            mppsscu.setTransferSyntaxes(CLIUtils.transferSyntaxesOf(cl));
117            mppsscu.setCodes(CLIUtils.loadProperties(
118                    cl.getOptionValue("code-config", "resource:code.properties"), null));
119            if (cl.hasOption("dc"))
120                mppsscu.setFinalStatus("DISCONTINUED");
121            if (cl.hasOption("dc-reason"))
122                mppsscu.setDiscontinuationReason(cl.getOptionValue("dc-reason"));
123            stgcmtscu.setTransferSyntaxes(CLIUtils.transferSyntaxesOf(cl));
124            stgcmtscu.setStorageDirectory(StgCmtSCU.getStorageDirectory(cl));
125            StoreSCU.configureRelatedSOPClass(storescu, cl);
126            storescu.setUIDSuffix(StoreSCU.uidSuffixOf(cl));
127            Attributes attrs = new Attributes();
128            CLIUtils.addAttributes(attrs, cl.getOptionValues("s"));
129            mppsscu.setAttributes(attrs);
130            storescu.setAttributes(attrs);
131            stgcmtscu.setAttributes(attrs);
132            setTlsParams(mppsscu.getRemoteConnection(), conn);
133            setTlsParams(storescu.getRemoteConnection(), conn);
134            setTlsParams(stgcmtscu.getRemoteConnection(), conn);
135            String tmpPrefix = "iocmtest-";
136            String tmpSuffix = null;
137            File tmpDir = null;
138            configureTmpFile(storescu, tmpPrefix, tmpSuffix, tmpDir, cl);
139            String mppsiuid = UIDUtils.createUID();
140            mppsscu.setPPSUID(mppsiuid);
141            if(cl.hasOption("kos-title")) {
142                List<String> fname = Arrays.asList(mkkos(cl));
143                scanFiles(fname, tmpPrefix, tmpSuffix, tmpDir, mppsscu, storescu, stgcmtscu);
144            } else {
145                stgcmtscu.setUIDSuffix(cl.getOptionValue("uid-suffix"));
146                storescu.setUIDSuffix(cl.getOptionValue("uid-suffix"));
147                mppsscu.setUIDSuffix(cl.getOptionValue("uid-suffix"));
148                scanFiles(cl.getArgList(), tmpPrefix, tmpSuffix, tmpDir, mppsscu, storescu, stgcmtscu);
149            }
150            ExecutorService executorService =
151                    Executors.newCachedThreadPool();
152            ScheduledExecutorService scheduledExecutorService =
153                    Executors.newSingleThreadScheduledExecutor();
154            device.setExecutor(executorService);
155            device.setScheduledExecutor(scheduledExecutorService);
156            device.bindConnections();
157            try {
158                boolean sendMpps = cl.hasOption("mpps");
159                boolean sendLateMpps = cl.hasOption("mpps-late");
160                if (sendMpps || sendLateMpps) {
161                    sendMpps(mppsscu, sendMpps);
162                    addReferencedPerformedProcedureStepSequence(mppsiuid, storescu);
163                } else {
164                    nullifyReferencedPerformedProcedureStepSequence(storescu);
165                }
166                sendObjects(storescu);
167                if (sendLateMpps)
168                    sendMppsNSet(mppsscu);
169                if (cl.hasOption("stgcmt"))
170                    sendStgCmt(stgcmtscu);
171            } finally {
172                if (conn.isListening()) {
173                    device.waitForNoOpenConnections();
174                    device.unbindConnections();
175                }
176                executorService.shutdown();
177                scheduledExecutorService.shutdown();
178            }
179        } catch (ParseException e) {
180            System.err.println(e.getMessage());
181            System.err.println(rb.getString("try"));
182            System.exit(2);
183        } catch (Exception e) {
184            System.err.println(e.getMessage());
185            e.printStackTrace();
186            System.exit(2);
187        }
188    }
189
190    private static void checkOptions(CommandLine cl) throws ParseException {
191        if (!cl.hasOption("b"))
192            throw new MissingOptionException(
193                    CLIUtils.rb.getString("missing-bind-opt"));
194        if (!cl.hasOption("c"))
195            throw new MissingOptionException(
196                    CLIUtils.rb.getString("missing-connect-opt"));
197        if (cl.hasOption("mpps") && cl.hasOption("mpps-late"))
198            throw new ParseException(rb.getString("mpps-error"));
199    }
200
201    public static void setTlsParams(Connection remote, Connection conn) {
202        remote.setTlsProtocols(conn.tlsProtocols());
203        remote.setTlsCipherSuites(conn.getTlsCipherSuites());
204    }
205
206    private static void addReferencedPerformedProcedureStepSequence(String mppsiuid,
207            StoreSCU storescu) {
208        Attributes attrs = storescu.getAttributes();
209        Sequence seq = attrs.newSequence(Tag.ReferencedPerformedProcedureStepSequence, 1);
210        Attributes item = new Attributes(2);
211        item.setString(Tag.ReferencedSOPClassUID, VR.UI, UID.ModalityPerformedProcedureStepSOPClass);
212        item.setString(Tag.ReferencedSOPInstanceUID, VR.UI, mppsiuid);
213        seq.add(item);
214    }
215
216    private static void nullifyReferencedPerformedProcedureStepSequence(StoreSCU storescu) {
217        Attributes attrs = storescu.getAttributes();
218        attrs.setNull(Tag.ReferencedPerformedProcedureStepSequence, VR.SQ);
219    }
220
221    @SuppressWarnings("unchecked")
222    private static String mkkos(CommandLine cl) throws Exception {
223        printNextStepMessage("Will now generate a Key Object for files in " + cl.getArgList());
224        final MkKOS mkkos = new MkKOS();
225        mkkos.setUIDSuffix(cl.getOptionValue("uid-suffix"));
226        mkkos.setCodes(CLIUtils.loadProperties(
227                cl.getOptionValue("code-config", "resource:code.properties"),
228                null));
229        mkkos.setDocumentTitle(mkkos.toCodeItem(documentTitleOf(cl)));
230        mkkos.setKeyObjectDescription(cl.getOptionValue("desc"));
231        mkkos.setSeriesNumber(cl.getOptionValue("series-no", "999"));
232        mkkos.setInstanceNumber(cl.getOptionValue("inst-no", "1"));
233        mkkos.setOutputFile(MkKOS.outputFileOf(cl));
234        mkkos.setNoFileMetaInformation(cl.hasOption("F"));
235        mkkos.setTransferSyntax(cl.getOptionValue("t", UID.ExplicitVRLittleEndian));
236        mkkos.setEncodingOptions(CLIUtils.encodingOptionsOf(cl));
237        DicomFiles.scan(cl.getArgList(), new DicomFiles.Callback() {
238
239            @Override
240            public boolean dicomFile(File f, Attributes fmi,
241                    long dsPos, Attributes ds) {
242                return mkkos.addInstance(ds);
243            }
244        });
245        System.out.println();
246        mkkos.writeKOS();
247        System.out.println(MessageFormat.format(rb.getString("stored"), mkkos.getFname()));
248        return mkkos.getFname();
249    }
250
251    private static String documentTitleOf(CommandLine cl) throws MissingOptionException {
252        if (!cl.hasOption("kos-title"))
253            throw new MissingOptionException(rb.getString("missing-title"));
254        return cl.getOptionValue("kos-title");
255    }
256
257    private static void sendStgCmt(StgCmtSCU stgcmtscu) throws IOException,
258            InterruptedException, IncompatibleConnectionException, GeneralSecurityException {
259        printNextStepMessage("Will now send Storage Commitment to " + calledAET);
260        try {
261            stgcmtscu.open();
262            stgcmtscu.sendRequests();
263        } finally {
264            stgcmtscu.close();
265        }
266    }
267
268    private static void sendMpps(MppsSCU mppsscu, boolean sendNSet) throws IOException,
269            InterruptedException, IncompatibleConnectionException, GeneralSecurityException {
270        try {
271            printNextStepMessage("Will now send MPPS N-CREATE to " + calledAET);
272            mppsscu.open();
273            mppsscu.createMpps();
274            if (sendNSet) {
275                printNextStepMessage("Will now send MPPS N-SET to " + calledAET);
276                mppsscu.updateMpps();
277            }
278        } finally {
279            mppsscu.close();
280        }
281    }
282
283    private static void sendMppsNSet(MppsSCU mppsscu) throws IOException, InterruptedException,
284            IncompatibleConnectionException, GeneralSecurityException {
285        try {
286            printNextStepMessage("Will now send MPPS N-SET to " + calledAET);
287            mppsscu.open();
288            mppsscu.updateMpps();
289        } finally {
290            mppsscu.close();
291        }
292    }
293
294    private static void printNextStepMessage(String message) throws IOException {
295        System.out.println("===========================================================");
296        System.out.println(message + ". Press <enter> to continue.");
297        System.out.println("===========================================================");
298        bufferedReader.read();
299    }
300
301    private static void sendObjects(StoreSCU storescu) throws IOException,
302            InterruptedException, IncompatibleConnectionException, GeneralSecurityException {
303        printNextStepMessage("Will now send DICOM object(s) to " + calledAET);
304        try {
305            storescu.open();
306            storescu.sendFiles();
307        } finally {
308            storescu.close();
309        }
310    }
311    
312    private static CommandLine parseComandLine(String[] args)
313            throws ParseException{
314        Options opts = new Options();
315        CLIUtils.addTransferSyntaxOptions(opts);
316        CLIUtils.addConnectOption(opts);
317        CLIUtils.addAEOptions(opts);
318        CLIUtils.addResponseTimeoutOption(opts);
319        CLIUtils.addCommonOptions(opts);
320        CLIUtils.addBindOption(opts, "IOCMTEST");
321        StoreSCU.addTmpFileOptions(opts);
322        StoreSCU.addUIDSuffixOption(opts);
323        addOptions(opts);
324        return CLIUtils.parseComandLine(args, opts, rb, Modality.class);
325    }
326    
327    @SuppressWarnings("static-access")
328    private static void addOptions(Options opts) {
329        opts.addOption(OptionBuilder
330                .hasArg()
331                .withArgName("code-value")
332                .withDescription(rb.getString("kos-title"))
333                .withLongOpt("kos-title")
334                .create());
335        opts.addOption(OptionBuilder
336                .hasArg()
337                .withArgName("file")
338                .withDescription(rb.getString("o-file"))
339                .create("o"));
340        opts.addOption(OptionBuilder
341                .hasArg()
342                .withArgName("file")
343                .withDescription(rb.getString("code-config"))
344                .withLongOpt("code-config")
345                .create());
346        OptionGroup mpps = new OptionGroup();
347        mpps.addOption(OptionBuilder
348                .withDescription(rb.getString("mpps-late"))
349                .withLongOpt("mpps-late")
350                .create());
351        mpps.addOption(OptionBuilder
352                .withDescription(rb.getString("mpps"))
353                .withLongOpt("mpps")
354                .create());
355        opts.addOptionGroup(mpps);
356        opts.addOption(OptionBuilder
357                .withDescription(rb.getString("stgcmt"))
358                .withLongOpt("stgcmt")
359                .create());
360        opts.addOption(null, "dc", false, rb.getString("dc"));
361        opts.addOption(OptionBuilder
362                .hasArg()
363                .withArgName("code-value")
364                .withDescription(rb.getString("dc-reason"))
365                .withLongOpt("dc-reason")
366                .create());
367        opts.addOption(OptionBuilder
368                .hasArgs()
369                .withArgName("[seq/]attr=value")
370                .withValueSeparator('=')
371                .withDescription(rb.getString("set"))
372                .create("s"));
373   }
374    
375    private static void scanFiles(List<String> fnames, String tmpPrefix, String tmpSuffix,
376            File tmpDir, final MppsSCU mppsscu, final StoreSCU storescu, final StgCmtSCU stgcmtscu)
377            throws IOException {
378        printNextStepMessage("Will now scan files in " + fnames);
379        File tmpFile = File.createTempFile(tmpPrefix, tmpSuffix, tmpDir);
380        tmpFile.deleteOnExit();
381        final BufferedWriter fileInfos = new BufferedWriter(new OutputStreamWriter(
382                new FileOutputStream(tmpFile)));
383        try {
384            DicomFiles.scan(fnames, new DicomFiles.Callback() {
385
386                @Override
387                public boolean dicomFile(File f, Attributes fmi, long dsPos,
388                        Attributes ds) throws Exception {
389                    return mppsscu.addInstance(ds)
390                        && storescu.addFile(fileInfos, f, dsPos, fmi, ds)
391                        && stgcmtscu.addInstance(ds);
392                }
393            });
394            storescu.setTmpFile(tmpFile);
395        } finally {
396            fileInfos.close();
397        }
398        System.out.println(" Done");
399    }
400    
401    private static void configureTmpFile(StoreSCU storescu, String tmpPrefix, String tmpSuffix,
402            File tmpDir, CommandLine cl) {
403        if (cl.hasOption("tmp-file-dir")) {
404            tmpDir = new File(cl.getOptionValue("tmp-file-dir"));
405            storescu.setTmpFileDirectory(tmpDir);
406        }
407        tmpPrefix = cl.getOptionValue("tmp-file-prefix", "iocmtest-");
408        storescu.setTmpFilePrefix(tmpPrefix);
409        tmpSuffix = cl.getOptionValue("tmp-file-suffix");
410        storescu.setTmpFileSuffix(tmpSuffix);
411    }
412}