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}