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.findscu; 040 041import java.io.BufferedOutputStream; 042import java.io.File; 043import java.io.FileOutputStream; 044import java.io.IOException; 045import java.io.OutputStream; 046import java.security.GeneralSecurityException; 047import java.text.DecimalFormat; 048import java.text.MessageFormat; 049import java.util.EnumSet; 050import java.util.List; 051import java.util.ResourceBundle; 052import java.util.Set; 053import java.util.concurrent.ExecutorService; 054import java.util.concurrent.Executors; 055import java.util.concurrent.ScheduledExecutorService; 056import java.util.concurrent.atomic.AtomicInteger; 057 058import javax.xml.transform.OutputKeys; 059import javax.xml.transform.Templates; 060import javax.xml.transform.TransformerFactory; 061import javax.xml.transform.sax.SAXTransformerFactory; 062import javax.xml.transform.sax.TransformerHandler; 063import javax.xml.transform.stream.StreamResult; 064import javax.xml.transform.stream.StreamSource; 065 066import org.apache.commons.cli.CommandLine; 067import org.apache.commons.cli.OptionBuilder; 068import org.apache.commons.cli.Options; 069import org.apache.commons.cli.ParseException; 070import org.dcm4che3.data.Attributes; 071import org.dcm4che3.data.Tag; 072import org.dcm4che3.data.UID; 073import org.dcm4che3.data.VR; 074import org.dcm4che3.io.DicomInputStream; 075import org.dcm4che3.io.DicomOutputStream; 076import org.dcm4che3.io.SAXWriter; 077import org.dcm4che3.net.*; 078import org.dcm4che3.net.pdu.AAssociateRQ; 079import org.dcm4che3.net.pdu.ExtendedNegotiation; 080import org.dcm4che3.net.pdu.PresentationContext; 081import org.dcm4che3.tool.common.CLIUtils; 082import org.dcm4che3.util.SafeClose; 083import org.dcm4che3.util.StringUtils; 084 085/** 086 * The findscu application implements a Service Class User (SCU) for the 087 * Query/Retrieve, the Modality Worklist Management, the Unified Worklist and 088 * Procedure Step, the Hanging Protocol Query/Retrieve and the Color Palette 089 * Query/Retrieve Service Class. findscu only supports query functionality using 090 * the C-FIND message. It sends query keys to an Service Class Provider (SCP) 091 * and waits for responses. 092 * 093 * @author Gunter Zeilinger <gunterze@gmail.com> 094 */ 095public class FindSCU { 096 097 public static enum InformationModel { 098 PatientRoot(UID.PatientRootQueryRetrieveInformationModelFIND, "STUDY"), 099 StudyRoot(UID.StudyRootQueryRetrieveInformationModelFIND, "STUDY"), 100 PatientStudyOnly(UID.PatientStudyOnlyQueryRetrieveInformationModelFINDRetired, "STUDY"), 101 MWL(UID.ModalityWorklistInformationModelFIND, null), 102 UPSPull(UID.UnifiedProcedureStepPullSOPClass, null), 103 UPSWatch(UID.UnifiedProcedureStepWatchSOPClass, null), 104 HangingProtocol(UID.HangingProtocolInformationModelFIND, null), 105 ColorPalette(UID.ColorPaletteQueryRetrieveInformationModelFIND, null); 106 107 final String cuid; 108 final String level; 109 110 InformationModel(String cuid, String level) { 111 this.cuid = cuid; 112 this.level = level; 113 } 114 115 public void adjustQueryOptions(EnumSet<QueryOption> queryOptions) { 116 if (level == null) { 117 queryOptions.add(QueryOption.RELATIONAL); 118 queryOptions.add(QueryOption.DATETIME); 119 } 120 } 121 122 public String getCuid() { 123 return cuid; 124 } 125 } 126 127 private static ResourceBundle rb = 128 ResourceBundle.getBundle("org.dcm4che3.tool.findscu.messages"); 129 private static SAXTransformerFactory saxtf; 130 131 private Device device = new Device("findscu"); 132 private ApplicationEntity ae = new ApplicationEntity("FINDSCU"); 133 private final Connection conn = new Connection(); 134 private final Connection remote = new Connection(); 135 private final AAssociateRQ rq = new AAssociateRQ(); 136 private int priority; 137 private int cancelAfter; 138 private InformationModel model; 139 private static String[] modelUIDandTS; 140 141 private File outDir; 142 private DecimalFormat outFileFormat; 143 private int[] inFilter; 144 private final Attributes keys = new Attributes(); 145 146 private boolean catOut = false; 147 private boolean xml = false; 148 private boolean xmlIndent = false; 149 private boolean xmlIncludeKeyword = true; 150 private boolean xmlIncludeNamespaceDeclaration = false; 151 private File xsltFile; 152 private Templates xsltTpls; 153 private OutputStream out; 154 155 private Association as; 156 private final AtomicInteger totNumMatches = new AtomicInteger(); 157 158 private long tStartCFind; 159 160 public FindSCU() throws IOException { 161 device.addConnection(conn); 162 device.addApplicationEntity(ae); 163 ae.addConnection(conn); 164 } 165 public FindSCU(ApplicationEntity appEntity) throws IOException { 166 this.ae = appEntity; 167 this.device = this.ae.getDevice(); 168 169 } 170 public final void setPriority(int priority) { 171 this.priority = priority; 172 } 173 174 public final void setInformationModel(InformationModel model, String[] tss, 175 EnumSet<QueryOption> queryOptions) { 176 this.model = model; 177 rq.addPresentationContext(new PresentationContext(1, model.cuid, tss)); 178 if (!queryOptions.isEmpty()) { 179 model.adjustQueryOptions(queryOptions); 180 rq.addExtendedNegotiation(new ExtendedNegotiation(model.cuid, 181 QueryOption.toExtendedNegotiationInformation(queryOptions))); 182 } 183 if (model.level != null) 184 addLevel(model.level); 185 } 186 187 public void addLevel(String s) { 188 keys.setString(Tag.QueryRetrieveLevel, VR.CS, s); 189 } 190 191 public final void setCancelAfter(int cancelAfter) { 192 this.cancelAfter = cancelAfter; 193 } 194 195 public final void setOutputDirectory(File outDir) { 196 outDir.mkdirs(); 197 this.outDir = outDir; 198 } 199 200 public final void setOutputFileFormat(String outFileFormat) { 201 this.outFileFormat = new DecimalFormat(outFileFormat); 202 } 203 204 public final void setXSLT(File xsltFile) { 205 this.xsltFile = xsltFile; 206 } 207 208 public final void setXML(boolean xml) { 209 this.xml = xml; 210 } 211 212 public final void setXMLIndent(boolean indent) { 213 this.xmlIndent = indent; 214 } 215 216 public final void setXMLIncludeKeyword(boolean includeKeyword) { 217 this.xmlIncludeKeyword = includeKeyword; 218 } 219 220 public final void setXMLIncludeNamespaceDeclaration( 221 boolean includeNamespaceDeclaration) { 222 this.xmlIncludeNamespaceDeclaration = includeNamespaceDeclaration; 223 } 224 225 public final void setConcatenateOutputFiles(boolean catOut) { 226 this.catOut = catOut; 227 } 228 229 public final void setInputFilter(int[] inFilter) { 230 this.inFilter = inFilter; 231 } 232 233 private static CommandLine parseComandLine(String[] args) 234 throws ParseException { 235 Options opts = new Options(); 236 addServiceClassOptions(opts); 237 addKeyOptions(opts); 238 addOutputOptions(opts); 239 addQueryLevelOption(opts); 240 addCancelOption(opts); 241 CLIUtils.addConnectOption(opts); 242 CLIUtils.addBindOption(opts, "FINDSCU"); 243 CLIUtils.addAEOptions(opts); 244 CLIUtils.addResponseTimeoutOption(opts); 245 CLIUtils.addPriorityOption(opts); 246 CLIUtils.addCommonOptions(opts); 247 return CLIUtils.parseComandLine(args, opts, rb, FindSCU.class); 248 } 249 250 @SuppressWarnings("static-access") 251 private static void addServiceClassOptions(Options opts) { 252 opts.addOption(OptionBuilder 253 .hasArg() 254 .withArgName("name") 255 .withDescription(rb.getString("model")) 256 .create("M")); 257 CLIUtils.addTransferSyntaxOptions(opts); 258 opts.addOption(null, "model-uid", true, rb.getString("model-uid")); 259 opts.addOption(null, "relational", false, rb.getString("relational")); 260 opts.addOption(null, "datetime", false, rb.getString("datetime")); 261 opts.addOption(null, "fuzzy", false, rb.getString("fuzzy")); 262 opts.addOption(null, "timezone", false, rb.getString("timezone")); 263 } 264 265 @SuppressWarnings("static-access") 266 private static void addQueryLevelOption(Options opts) { 267 opts.addOption(OptionBuilder 268 .hasArg() 269 .withArgName("PATIENT|STUDY|SERIES|IMAGE") 270 .withDescription(rb.getString("level")) 271 .create("L")); 272 } 273 274 @SuppressWarnings("static-access") 275 private static void addCancelOption(Options opts) { 276 opts.addOption(OptionBuilder 277 .withLongOpt("cancel") 278 .hasArg() 279 .withArgName("num-matches") 280 .withDescription(rb.getString("cancel")) 281 .create()); 282 } 283 284 @SuppressWarnings("static-access") 285 private static void addKeyOptions(Options opts) { 286 opts.addOption(OptionBuilder 287 .hasArgs() 288 .withArgName("[seq/]attr=value") 289 .withValueSeparator('=') 290 .withDescription(rb.getString("match")) 291 .create("m")); 292 opts.addOption(OptionBuilder 293 .hasArgs() 294 .withArgName("[seq/]attr") 295 .withDescription(rb.getString("return")) 296 .create("r")); 297 opts.addOption(OptionBuilder 298 .hasArgs() 299 .withArgName("attr") 300 .withDescription(rb.getString("in-attr")) 301 .create("i")); 302 } 303 304 @SuppressWarnings("static-access") 305 private static void addOutputOptions(Options opts) { 306 opts.addOption(OptionBuilder 307 .withLongOpt("out-dir") 308 .hasArg() 309 .withArgName("directory") 310 .withDescription(rb.getString("out-dir")) 311 .create()); 312 opts.addOption(OptionBuilder 313 .withLongOpt("out-file") 314 .hasArg() 315 .withArgName("name") 316 .withDescription(rb.getString("out-file")) 317 .create()); 318 opts.addOption("X", "xml", false, rb.getString("xml")); 319 opts.addOption(OptionBuilder 320 .withLongOpt("xsl") 321 .hasArg() 322 .withArgName("xsl-file") 323 .withDescription(rb.getString("xsl")) 324 .create("x")); 325 opts.addOption("I", "indent", false, rb.getString("indent")); 326 opts.addOption("K", "no-keyword", false, rb.getString("no-keyword")); 327 opts.addOption(null, "xmlns", false, rb.getString("xmlns")); 328 opts.addOption(null, "out-cat", false, rb.getString("out-cat")); 329 } 330 331 public ApplicationEntity getApplicationEntity() { 332 return ae; 333 } 334 335 public Connection getRemoteConnection() { 336 return remote; 337 } 338 339 public AAssociateRQ getAAssociateRQ() { 340 return rq; 341 } 342 343 public Association getAssociation() { 344 return as; 345 } 346 347 public Device getDevice() { 348 return device; 349 } 350 351 public Attributes getKeys() { 352 return keys; 353 } 354 355 @SuppressWarnings("unchecked") 356 public static void main(String[] args) { 357 try { 358 CommandLine cl = parseComandLine(args); 359 FindSCU main = new FindSCU(); 360 CLIUtils.configureConnect(main.remote, main.rq, cl); 361 CLIUtils.configureBind(main.conn, main.ae, cl); 362 CLIUtils.configure(main.conn, cl); 363 main.remote.setTlsProtocols(main.conn.tlsProtocols()); 364 main.remote.setTlsCipherSuites(main.conn.getTlsCipherSuites()); 365 configureServiceClass(main, cl); 366 configureKeys(main, cl); 367 configureOutput(main, cl); 368 configureCancel(main, cl); 369 main.setPriority(CLIUtils.priorityOf(cl)); 370 ExecutorService executorService = 371 Executors.newSingleThreadExecutor(); 372 ScheduledExecutorService scheduledExecutorService = 373 Executors.newSingleThreadScheduledExecutor(); 374 main.device.setExecutor(executorService); 375 main.device.setScheduledExecutor(scheduledExecutorService); 376 try { 377 long t1 = System.currentTimeMillis(); 378 main.open(); 379 long t2 = System.currentTimeMillis(); 380 System.out.println("Association opened in "+(t2-t1)+"ms"); 381 List<String> argList = cl.getArgList(); 382 if (argList.isEmpty()) 383 main.query(); 384 else 385 for (String arg : argList) 386 main.query(new File(arg)); 387 } finally { 388 main.close(); 389 executorService.shutdown(); 390 scheduledExecutorService.shutdown(); 391 } 392 } catch (ParseException e) { 393 System.err.println("findscu: " + e.getMessage()); 394 System.err.println(rb.getString("try")); 395 System.exit(2); 396 } catch (Exception e) { 397 System.err.println("findscu: " + e.getMessage()); 398 e.printStackTrace(); 399 System.exit(2); 400 } 401 } 402 403 private static EnumSet<QueryOption> queryOptionsOf(FindSCU main, CommandLine cl) { 404 EnumSet<QueryOption> queryOptions = EnumSet.noneOf(QueryOption.class); 405 if (cl.hasOption("relational")) 406 queryOptions.add(QueryOption.RELATIONAL); 407 if (cl.hasOption("datetime")) 408 queryOptions.add(QueryOption.DATETIME); 409 if (cl.hasOption("fuzzy")) 410 queryOptions.add(QueryOption.FUZZY); 411 if (cl.hasOption("timezone")) 412 queryOptions.add(QueryOption.TIMEZONE); 413 return queryOptions; 414 } 415 416 private static void configureOutput(FindSCU main, CommandLine cl) { 417 if (cl.hasOption("out-dir")) 418 main.setOutputDirectory(new File(cl.getOptionValue("out-dir"))); 419 main.setOutputFileFormat(cl.getOptionValue("out-file", "000'.dcm'")); 420 main.setConcatenateOutputFiles(cl.hasOption("out-cat")); 421 main.setXML(cl.hasOption("X")); 422 if (cl.hasOption("x")) { 423 main.setXML(true); 424 main.setXSLT(new File(cl.getOptionValue("x"))); 425 } 426 main.setXMLIndent(cl.hasOption("I")); 427 main.setXMLIncludeKeyword(!cl.hasOption("K")); 428 main.setXMLIncludeNamespaceDeclaration(cl.hasOption("xmlns")); 429 } 430 431 private static void configureCancel(FindSCU main, CommandLine cl) { 432 if (cl.hasOption("cancel")) 433 main.setCancelAfter(Integer.parseInt(cl.getOptionValue("cancel"))); 434 } 435 436 private static void configureKeys(FindSCU main, CommandLine cl) { 437 CLIUtils.addEmptyAttributes(main.keys, cl.getOptionValues("r")); 438 CLIUtils.addAttributes(main.keys, cl.getOptionValues("m")); 439 if (cl.hasOption("L")) 440 main.addLevel(cl.getOptionValue("L")); 441 if (cl.hasOption("i")) 442 main.setInputFilter(CLIUtils.toTags(cl.getOptionValues("i"))); 443 } 444 445 private static void configureServiceClass(FindSCU main, CommandLine cl) throws ParseException { 446 main.setInformationModel(informationModelOf(cl), 447 CLIUtils.transferSyntaxesOf(cl), queryOptionsOf(main, cl)); 448 if (cl.hasOption("model-uid")) { 449 String cuidAndTS = cl.getOptionValue("model-uid"); 450 modelUIDandTS = StringUtils.split(cuidAndTS, '|'); 451 main.rq.addPresentationContext(new PresentationContext(3, modelUIDandTS[0], UID.ImplicitVRLittleEndian)); 452 main.rq.addPresentationContext(new PresentationContext(5, modelUIDandTS[0], UID.ExplicitVRLittleEndian)); 453 for (int i = 1, pcid = 7 ; i < modelUIDandTS.length ; i++) { 454 main.rq.addPresentationContext(new PresentationContext(pcid, modelUIDandTS[0], modelUIDandTS[i])); 455 pcid += 2; 456 } 457 } 458 } 459 460 private static InformationModel informationModelOf(CommandLine cl) throws ParseException { 461 try { 462 return cl.hasOption("M") 463 ? InformationModel.valueOf(cl.getOptionValue("M")) 464 : InformationModel.StudyRoot; 465 } catch(IllegalArgumentException e) { 466 throw new ParseException( 467 MessageFormat.format( 468 rb.getString("invalid-model-name"), 469 cl.getOptionValue("M"))); 470 } 471 } 472 473 public void open() throws IOException, InterruptedException, 474 IncompatibleConnectionException, GeneralSecurityException { 475 as = ae.connect(remote, rq); 476 } 477 478 public void close() throws IOException, InterruptedException { 479 if (as != null && as.isReadyForDataTransfer()) { 480 as.waitForOutstandingRSP(); 481 as.release(); 482 } 483 SafeClose.close(out); 484 out = null; 485 } 486 487 public void query(File f) throws IOException, InterruptedException { 488 Attributes attrs; 489 DicomInputStream dis = null; 490 try { 491 dis = new DicomInputStream(f); 492 attrs = dis.readDataset(-1, -1); 493 if (inFilter != null) { 494 attrs = new Attributes(inFilter.length + 1); 495 attrs.addSelected(attrs, inFilter); 496 } 497 } finally { 498 SafeClose.close(dis); 499 } 500 attrs.addAll(keys); 501 query(attrs); 502 } 503 504 505 public void query() throws IOException, InterruptedException { 506 query(keys); 507 } 508 509 private void query(Attributes keys) throws IOException, InterruptedException { 510 DimseRSPHandler rspHandler = new DimseRSPHandler(as.nextMessageID()) { 511 512 int cancelAfter = FindSCU.this.cancelAfter; 513 int numMatches; 514 515 @Override 516 public void onDimseRSP(Association as, Attributes cmd, 517 Attributes data) { 518 System.out.println("####### DimesRSP received after "+(System.currentTimeMillis()-tStartCFind)+"ms"); 519 super.onDimseRSP(as, cmd, data); 520 int status = cmd.getInt(Tag.Status, -1); 521 if (Status.isPending(status)) { 522 FindSCU.this.onResult(data); 523 ++numMatches; 524 if (cancelAfter != 0 && numMatches >= cancelAfter) 525 try { 526 cancel(as); 527 cancelAfter = 0; 528 } catch (IOException e) { 529 e.printStackTrace(); 530 } 531 } 532 } 533 }; 534 535 query(keys, rspHandler); 536 } 537 538 public void query( DimseRSPHandler rspHandler) throws IOException, InterruptedException { 539 query(keys, rspHandler); 540 } 541 542 private void query(Attributes keys, DimseRSPHandler rspHandler) throws IOException, InterruptedException { 543 String cuid = model.cuid; 544 if (modelUIDandTS != null) { 545 Set<String> ts = as.getTransferSyntaxesFor(modelUIDandTS[0]); 546 if (ts.size() > 0) { 547 cuid = modelUIDandTS[0]; 548 } 549 } 550 tStartCFind = System.currentTimeMillis(); 551 as.cfind(cuid, priority, keys, null, rspHandler); 552 long t2 = System.currentTimeMillis(); 553 System.out.println("C-FIND Request done in "+(t2-tStartCFind)+"ms!"); 554 } 555 556 private void onResult(Attributes data) { 557 int numMatches = totNumMatches.incrementAndGet(); 558 if (outDir == null) 559 return; 560 561 try { 562 if (out == null) { 563 File f = new File(outDir, fname(numMatches)); 564 out = new BufferedOutputStream( 565 new FileOutputStream(f)); 566 } 567 if (xml) { 568 writeAsXML(data, out); 569 } else { 570 DicomOutputStream dos = 571 new DicomOutputStream(out, UID.ImplicitVRLittleEndian); 572 dos.writeDataset(null, data); 573 } 574 out.flush(); 575 } catch (Exception e) { 576 e.printStackTrace(); 577 SafeClose.close(out); 578 out = null; 579 } finally { 580 if (!catOut) { 581 SafeClose.close(out); 582 out = null; 583 } 584 } 585 } 586 587 private String fname(int i) { 588 synchronized (outFileFormat) { 589 return outFileFormat.format(i); 590 } 591 } 592 593 private void writeAsXML(Attributes attrs, OutputStream out) throws Exception { 594 TransformerHandler th = getTransformerHandler(); 595 th.getTransformer().setOutputProperty(OutputKeys.INDENT, 596 xmlIndent ? "yes" : "no"); 597 th.setResult(new StreamResult(out)); 598 SAXWriter saxWriter = new SAXWriter(th); 599 saxWriter.setIncludeKeyword(xmlIncludeKeyword); 600 saxWriter.setIncludeNamespaceDeclaration(xmlIncludeNamespaceDeclaration); 601 saxWriter.write(attrs); 602 } 603 604 private TransformerHandler getTransformerHandler() throws Exception { 605 SAXTransformerFactory tf = saxtf; 606 if (tf == null) 607 saxtf = tf = (SAXTransformerFactory) TransformerFactory 608 .newInstance(); 609 if (xsltFile == null) 610 return tf.newTransformerHandler(); 611 612 Templates tpls = xsltTpls; 613 if (tpls == null) 614 xsltTpls = tpls = tf.newTemplates(new StreamSource(xsltFile)); 615 616 return tf.newTransformerHandler(tpls); 617 } 618 619 620}