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.storescu; 040 041import java.io.*; 042import java.security.GeneralSecurityException; 043import java.text.MessageFormat; 044import java.util.*; 045import java.util.concurrent.ExecutorService; 046import java.util.concurrent.Executors; 047import java.util.concurrent.ScheduledExecutorService; 048 049import javax.xml.parsers.ParserConfigurationException; 050 051import org.apache.commons.cli.*; 052import org.dcm4che3.data.Attributes; 053import org.dcm4che3.data.Tag; 054import org.dcm4che3.data.UID; 055import org.dcm4che3.imageio.codec.Decompressor; 056import org.dcm4che3.io.DicomInputStream; 057import org.dcm4che3.io.DicomInputStream.IncludeBulkData; 058import org.dcm4che3.io.SAXReader; 059import org.dcm4che3.net.ApplicationEntity; 060import org.dcm4che3.net.Association; 061import org.dcm4che3.net.Connection; 062import org.dcm4che3.net.DataWriterAdapter; 063import org.dcm4che3.net.Device; 064import org.dcm4che3.net.DimseRSP; 065import org.dcm4che3.net.DimseRSPHandler; 066import org.dcm4che3.net.IncompatibleConnectionException; 067import org.dcm4che3.net.InputStreamDataWriter; 068import org.dcm4che3.net.Status; 069import org.dcm4che3.net.pdu.AAssociateRQ; 070import org.dcm4che3.net.pdu.PresentationContext; 071import org.dcm4che3.tool.common.CLIUtils; 072import org.dcm4che3.tool.common.DicomFiles; 073import org.dcm4che3.util.SafeClose; 074import org.dcm4che3.util.StringUtils; 075import org.dcm4che3.util.TagUtils; 076import org.xml.sax.SAXException; 077 078/** 079 * @author Gunter Zeilinger <gunterze@gmail.com> 080 * @author Michael Backhaus <michael.backhaus@agfa.com> 081 */ 082public class StoreSCU { 083 084 public interface RSPHandlerFactory { 085 086 DimseRSPHandler createDimseRSPHandler(File f); 087 } 088 089 private static ResourceBundle rb = ResourceBundle 090 .getBundle("org.dcm4che3.tool.storescu.messages"); 091 092 private final ApplicationEntity ae; 093 private final Connection remote; 094 private final AAssociateRQ rq = new AAssociateRQ(); 095 private final RelatedGeneralSOPClasses relSOPClasses = new RelatedGeneralSOPClasses(); 096 private Attributes attrs; 097 private String uidSuffix; 098 private boolean relExtNeg; 099 private int priority; 100 private String tmpPrefix = "storescu-"; 101 private String tmpSuffix; 102 private File tmpDir; 103 private File tmpFile; 104 private String inputFile; 105 private Association as; 106 107 private long totalSize; 108 private int filesScanned; 109 private int filesSent; 110 111 private RSPHandlerFactory rspHandlerFactory = new RSPHandlerFactory() { 112 113 @Override 114 public DimseRSPHandler createDimseRSPHandler(final File f) { 115 116 return new DimseRSPHandler(as.nextMessageID()) { 117 118 @Override 119 public void onDimseRSP(Association as, Attributes cmd, 120 Attributes data) { 121 super.onDimseRSP(as, cmd, data); 122 StoreSCU.this.onCStoreRSP(cmd, f); 123 } 124 }; 125 } 126 }; 127 128 public StoreSCU(ApplicationEntity ae) throws IOException { 129 this.remote = new Connection(); 130 this.ae = ae; 131 rq.addPresentationContext(new PresentationContext(1, 132 UID.VerificationSOPClass, UID.ImplicitVRLittleEndian)); 133 } 134 135 public void setRspHandlerFactory(RSPHandlerFactory rspHandlerFactory) { 136 this.rspHandlerFactory = rspHandlerFactory; 137 } 138 139 public AAssociateRQ getAAssociateRQ() { 140 return rq; 141 } 142 143 public Connection getRemoteConnection() { 144 return remote; 145 } 146 147 public Attributes getAttributes() { 148 return attrs; 149 } 150 151 public void setAttributes(Attributes attrs) { 152 this.attrs = attrs; 153 } 154 155 public void setTmpFile(File tmpFile) { 156 this.tmpFile = tmpFile; 157 } 158 159 public final void setPriority(int priority) { 160 this.priority = priority; 161 } 162 163 public final void setUIDSuffix(String uidSuffix) { 164 this.uidSuffix = uidSuffix; 165 } 166 167 public final void setTmpFilePrefix(String prefix) { 168 this.tmpPrefix = prefix; 169 } 170 171 public final void setTmpFileSuffix(String suffix) { 172 this.tmpSuffix = suffix; 173 } 174 175 public final void setTmpFileDirectory(File tmpDir) { 176 this.tmpDir = tmpDir; 177 } 178 179 private static CommandLine parseComandLine(String[] args) 180 throws ParseException { 181 Options opts = new Options(); 182 CLIUtils.addConnectOption(opts); 183 CLIUtils.addBindOption(opts, "STORESCU"); 184 CLIUtils.addAEOptions(opts); 185 CLIUtils.addResponseTimeoutOption(opts); 186 CLIUtils.addPriorityOption(opts); 187 CLIUtils.addCommonOptions(opts); 188 addTmpFileOptions(opts); 189 addRelatedSOPClassOptions(opts); 190 addAttributesOption(opts); 191 addUIDSuffixOption(opts); 192 addInputFileOption(opts); 193 return CLIUtils.parseComandLine(args, opts, rb, StoreSCU.class); 194 } 195 196 @SuppressWarnings("static-access") 197 private static void addAttributesOption(Options opts) { 198 opts.addOption(OptionBuilder.hasArgs().withArgName("[seq/]attr=value") 199 .withValueSeparator('=').withDescription(rb.getString("set")) 200 .create("s")); 201 } 202 203 @SuppressWarnings("static-access") 204 public static void addUIDSuffixOption(Options opts) { 205 opts.addOption(OptionBuilder.hasArg().withArgName("suffix") 206 .withDescription(rb.getString("uid-suffix")) 207 .withLongOpt("uid-suffix").create(null)); 208 } 209 210 @SuppressWarnings("static-access") 211 public static void addTmpFileOptions(Options opts) { 212 opts.addOption(OptionBuilder.hasArg().withArgName("directory") 213 .withDescription(rb.getString("tmp-file-dir")) 214 .withLongOpt("tmp-file-dir").create(null)); 215 opts.addOption(OptionBuilder.hasArg().withArgName("prefix") 216 .withDescription(rb.getString("tmp-file-prefix")) 217 .withLongOpt("tmp-file-prefix").create(null)); 218 opts.addOption(OptionBuilder.hasArg().withArgName("suffix") 219 .withDescription(rb.getString("tmp-file-suffix")) 220 .withLongOpt("tmp-file-suffix").create(null)); 221 } 222 223 @SuppressWarnings("static-access") 224 private static void addRelatedSOPClassOptions(Options opts) { 225 opts.addOption(null, "rel-ext-neg", false, rb.getString("rel-ext-neg")); 226 opts.addOption(OptionBuilder.hasArg().withArgName("file|url") 227 .withDescription(rb.getString("rel-sop-classes")) 228 .withLongOpt("rel-sop-classes").create(null)); 229 } 230 231 @SuppressWarnings("static-access") 232 private static void addInputFileOption(Options opts) { 233 opts.addOption(OptionBuilder.hasArg().withArgName("file").withDescription(rb.getString("input-file")) 234 .withLongOpt("input-file").create(null)); 235 236 } 237 238 @SuppressWarnings("unchecked") 239 public static void main(String[] args) { 240 long t1, t2; 241 try { 242 CommandLine cl = parseComandLine(args); 243 Device device = new Device("storescu"); 244 Connection conn = new Connection(); 245 device.addConnection(conn); 246 ApplicationEntity ae = new ApplicationEntity("STORESCU"); 247 device.addApplicationEntity(ae); 248 ae.addConnection(conn); 249 StoreSCU main = new StoreSCU(ae); 250 configureTmpFile(main, cl); 251 configureInputFile(main, cl); 252 CLIUtils.configureConnect(main.remote, main.rq, cl); 253 CLIUtils.configureBind(conn, ae, cl); 254 CLIUtils.configure(conn, cl); 255 main.remote.setTlsProtocols(conn.tlsProtocols()); 256 main.remote.setTlsCipherSuites(conn.getTlsCipherSuites()); 257 configureRelatedSOPClass(main, cl); 258 main.setAttributes(new Attributes()); 259 CLIUtils.addAttributes(main.attrs, cl.getOptionValues("s")); 260 main.setUIDSuffix(cl.getOptionValue("uid-suffix")); 261 main.setPriority(CLIUtils.priorityOf(cl)); 262 List<String> argList = cl.getArgList(); 263 boolean echo = argList.isEmpty() && main.inputFile == null; 264 if (!echo) { 265 System.out.println(rb.getString("scanning")); 266 t1 = System.currentTimeMillis(); 267 if (main.inputFile != null) { 268 main.scanFiles(getFilePathsFromInputFile(main.inputFile)); 269 } else { 270 main.scanFiles(argList); 271 } 272 t2 = System.currentTimeMillis(); 273 int n = main.filesScanned; 274 System.out.println(); 275 if (n == 0) 276 return; 277 System.out.println(MessageFormat.format( 278 rb.getString("scanned"), n, (t2 - t1) / 1000F, 279 (t2 - t1) / n)); 280 } 281 ExecutorService executorService = Executors 282 .newSingleThreadExecutor(); 283 ScheduledExecutorService scheduledExecutorService = Executors 284 .newSingleThreadScheduledExecutor(); 285 device.setExecutor(executorService); 286 device.setScheduledExecutor(scheduledExecutorService); 287 try { 288 t1 = System.currentTimeMillis(); 289 main.open(); 290 t2 = System.currentTimeMillis(); 291 System.out.println(MessageFormat.format( 292 rb.getString("connected"), main.as.getRemoteAET(), t2 293 - t1)); 294 if (echo) 295 main.echo(); 296 else { 297 t1 = System.currentTimeMillis(); 298 main.sendFiles(); 299 t2 = System.currentTimeMillis(); 300 } 301 } finally { 302 main.close(); 303 executorService.shutdown(); 304 scheduledExecutorService.shutdown(); 305 } 306 if (main.filesScanned > 0) { 307 float s = (t2 - t1) / 1000F; 308 float mb = main.totalSize / 1048576F; 309 System.out.println(MessageFormat.format(rb.getString("sent"), 310 main.filesSent, mb, s, mb / s)); 311 } 312 } catch (ParseException e) { 313 System.err.println("storescu: " + e.getMessage()); 314 System.err.println(rb.getString("try")); 315 System.exit(2); 316 } catch (Exception e) { 317 System.err.println("storescu: " + e.getMessage()); 318 e.printStackTrace(); 319 System.exit(2); 320 } 321 } 322 323 public static String uidSuffixOf(CommandLine cl) { 324 return cl.getOptionValue("uid-suffix"); 325 } 326 327 private static void configureTmpFile(StoreSCU storescu, CommandLine cl) { 328 if (cl.hasOption("tmp-file-dir")) 329 storescu.setTmpFileDirectory(new File(cl 330 .getOptionValue("tmp-file-dir"))); 331 storescu.setTmpFilePrefix(cl.getOptionValue("tmp-file-prefix", 332 "storescu-")); 333 storescu.setTmpFileSuffix(cl.getOptionValue("tmp-file-suffix")); 334 } 335 336 private static void configureInputFile(StoreSCU storescu, CommandLine cl) { 337 if (cl.hasOption("input-file")) { 338 storescu.inputFile = cl.getOptionValue("input-file"); 339 } 340 } 341 342 public static void configureRelatedSOPClass(StoreSCU storescu, 343 CommandLine cl) throws IOException { 344 if (cl.hasOption("rel-ext-neg")) { 345 storescu.enableSOPClassRelationshipExtNeg(true); 346 Properties p = new Properties(); 347 CLIUtils.loadProperties( 348 cl.getOptionValue("rel-sop-classes", "resource:rel-sop-classes.properties"), 349 p); 350 storescu.relSOPClasses.init(p); 351 } 352 } 353 354 private static List<String> getFilePathsFromInputFile(String inputFile) throws IOException { 355 BufferedReader br = null; 356 List<String> inputFiles = new ArrayList<String>(); 357 358 try { 359 br = new BufferedReader(new FileReader(inputFile)); 360 String line; 361 while ((line = br.readLine()) != null) { 362 inputFiles.add(line); 363 } 364 } finally { 365 SafeClose.close(br); 366 } 367 368 return inputFiles; 369 } 370 371 public final void enableSOPClassRelationshipExtNeg(boolean enable) { 372 relExtNeg = enable; 373 } 374 375 public void scanFiles(List<String> fnames) throws IOException { 376 this.scanFiles(fnames, true); 377 } 378 379 public void scanFiles(List<String> fnames, boolean printout) 380 throws IOException { 381 tmpFile = File.createTempFile(tmpPrefix, tmpSuffix, tmpDir); 382 tmpFile.deleteOnExit(); 383 final BufferedWriter fileInfos = new BufferedWriter( 384 new OutputStreamWriter(new FileOutputStream(tmpFile))); 385 try { 386 DicomFiles.scan(fnames, printout, new DicomFiles.Callback() { 387 388 @Override 389 public boolean dicomFile(File f, Attributes fmi, long dsPos, 390 Attributes ds) throws IOException { 391 if (!addFile(fileInfos, f, dsPos, fmi, ds)) 392 return false; 393 394 filesScanned++; 395 return true; 396 } 397 }); 398 } finally { 399 fileInfos.close(); 400 } 401 } 402 403 public void sendFiles() throws IOException { 404 BufferedReader fileInfos = new BufferedReader(new InputStreamReader( 405 new FileInputStream(tmpFile))); 406 try { 407 String line; 408 while (as.isReadyForDataTransfer() 409 && (line = fileInfos.readLine()) != null) { 410 String[] ss = StringUtils.split(line, '\t'); 411 try { 412 send(new File(ss[4]), Long.parseLong(ss[3]), ss[1], ss[0], 413 ss[2]); 414 } catch (Exception e) { 415 e.printStackTrace(); 416 } 417 } 418 419 as.releaseGracefully(); 420 421 } finally { 422 SafeClose.close(fileInfos); 423 } 424 } 425 426 public boolean addFile(BufferedWriter fileInfos, File f, long endFmi, 427 Attributes fmi, Attributes ds) throws IOException { 428 String cuid = fmi.getString(Tag.MediaStorageSOPClassUID); 429 String iuid = fmi.getString(Tag.MediaStorageSOPInstanceUID); 430 String ts = fmi.getString(Tag.TransferSyntaxUID); 431 if (cuid == null || iuid == null) 432 return false; 433 434 fileInfos.write(iuid); 435 fileInfos.write('\t'); 436 fileInfos.write(cuid); 437 fileInfos.write('\t'); 438 fileInfos.write(ts); 439 fileInfos.write('\t'); 440 fileInfos.write(Long.toString(endFmi)); 441 fileInfos.write('\t'); 442 fileInfos.write(f.getPath()); 443 fileInfos.newLine(); 444 445 if (rq.containsPresentationContextFor(cuid, ts)) 446 return true; 447 448 if (!rq.containsPresentationContextFor(cuid)) { 449 if (relExtNeg) 450 rq.addCommonExtendedNegotiation(relSOPClasses 451 .getCommonExtendedNegotiation(cuid)); 452 if (!ts.equals(UID.ExplicitVRLittleEndian)) 453 rq.addPresentationContext(new PresentationContext(rq 454 .getNumberOfPresentationContexts() * 2 + 1, cuid, 455 UID.ExplicitVRLittleEndian)); 456 if (!ts.equals(UID.ImplicitVRLittleEndian)) 457 rq.addPresentationContext(new PresentationContext(rq 458 .getNumberOfPresentationContexts() * 2 + 1, cuid, 459 UID.ImplicitVRLittleEndian)); 460 } 461 rq.addPresentationContext(new PresentationContext(rq 462 .getNumberOfPresentationContexts() * 2 + 1, cuid, ts)); 463 return true; 464 } 465 466 public Attributes echo() throws IOException, InterruptedException { 467 DimseRSP response = as.cecho(); 468 response.next(); 469 return response.getCommand(); 470 } 471 472 public void send(final File f, long fmiEndPos, String cuid, String iuid, 473 String filets) throws IOException, InterruptedException, 474 ParserConfigurationException, SAXException { 475 String ts = selectTransferSyntax(cuid, filets); 476 477 if (f.getName().endsWith(".xml")) { 478 Attributes parsedDicomFile = SAXReader.parse(new FileInputStream(f)); 479 if (CLIUtils.updateAttributes(parsedDicomFile, attrs, uidSuffix)) 480 iuid = parsedDicomFile.getString(Tag.SOPInstanceUID); 481 if (!ts.equals(filets)) { 482 Decompressor.decompress(parsedDicomFile, filets); 483 } 484 as.cstore(cuid, iuid, priority, 485 new DataWriterAdapter(parsedDicomFile), ts, 486 rspHandlerFactory.createDimseRSPHandler(f)); 487 } else { 488 if (uidSuffix == null && attrs.isEmpty() && ts.equals(filets)) { 489 FileInputStream in = new FileInputStream(f); 490 try { 491 in.skip(fmiEndPos); 492 InputStreamDataWriter data = new InputStreamDataWriter(in); 493 as.cstore(cuid, iuid, priority, data, ts, 494 rspHandlerFactory.createDimseRSPHandler(f)); 495 } finally { 496 SafeClose.close(in); 497 } 498 } else { 499 DicomInputStream in = new DicomInputStream(f); 500 try { 501 in.setIncludeBulkData(IncludeBulkData.URI); 502 Attributes data = in.readDataset(-1, -1); 503 if (CLIUtils.updateAttributes(data, attrs, uidSuffix)) 504 iuid = data.getString(Tag.SOPInstanceUID); 505 if (!ts.equals(filets)) { 506 Decompressor.decompress(data, filets); 507 } 508 as.cstore(cuid, iuid, priority, 509 new DataWriterAdapter(data), ts, 510 rspHandlerFactory.createDimseRSPHandler(f)); 511 } finally { 512 SafeClose.close(in); 513 } 514 } 515 } 516 } 517 518 private String selectTransferSyntax(String cuid, String filets) { 519 Set<String> tss = as.getTransferSyntaxesFor(cuid); 520 if (tss.contains(filets)) 521 return filets; 522 523 if (tss.contains(UID.ExplicitVRLittleEndian)) 524 return UID.ExplicitVRLittleEndian; 525 526 return UID.ImplicitVRLittleEndian; 527 } 528 529 public void close() throws IOException, InterruptedException { 530 if (as != null) { 531 if (as.isReadyForDataTransfer()) 532 as.release(); 533 as.waitForSocketClose(); 534 } 535 } 536 537 public void open() throws IOException, InterruptedException, 538 IncompatibleConnectionException, GeneralSecurityException { 539 as = ae.connect(remote, rq); 540 } 541 542 private void onCStoreRSP(Attributes cmd, File f) { 543 int status = cmd.getInt(Tag.Status, -1); 544 switch (status) { 545 case Status.Success: 546 totalSize += f.length(); 547 ++filesSent; 548 System.out.print('.'); 549 break; 550 case Status.CoercionOfDataElements: 551 case Status.ElementsDiscarded: 552 case Status.DataSetDoesNotMatchSOPClassWarning: 553 totalSize += f.length(); 554 ++filesSent; 555 System.err.println(MessageFormat.format(rb.getString("warning"), 556 TagUtils.shortToHexString(status), f)); 557 System.err.println(cmd); 558 break; 559 default: 560 System.out.print('E'); 561 System.err.println(MessageFormat.format(rb.getString("error"), 562 TagUtils.shortToHexString(status), f)); 563 System.err.println(cmd); 564 } 565 } 566}