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}