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.getscu; 040 041import java.io.File; 042import java.io.IOException; 043import java.security.GeneralSecurityException; 044import java.text.MessageFormat; 045import java.util.List; 046import java.util.Map.Entry; 047import java.util.EnumSet; 048import java.util.Properties; 049import java.util.ResourceBundle; 050import java.util.Set; 051import java.util.concurrent.ExecutorService; 052import java.util.concurrent.Executors; 053import java.util.concurrent.ScheduledExecutorService; 054 055import org.apache.commons.cli.CommandLine; 056import org.apache.commons.cli.OptionBuilder; 057import org.apache.commons.cli.Options; 058import org.apache.commons.cli.ParseException; 059import org.dcm4che3.data.Tag; 060import org.dcm4che3.data.UID; 061import org.dcm4che3.data.Attributes; 062import org.dcm4che3.data.ElementDictionary; 063import org.dcm4che3.data.VR; 064import org.dcm4che3.io.DicomInputStream; 065import org.dcm4che3.io.DicomOutputStream; 066import org.dcm4che3.net.ApplicationEntity; 067import org.dcm4che3.net.Association; 068import org.dcm4che3.net.Connection; 069import org.dcm4che3.net.Device; 070import org.dcm4che3.net.DimseRSPHandler; 071import org.dcm4che3.net.IncompatibleConnectionException; 072import org.dcm4che3.net.PDVInputStream; 073import org.dcm4che3.net.QueryOption; 074import org.dcm4che3.net.Status; 075import org.dcm4che3.net.pdu.AAssociateRQ; 076import org.dcm4che3.net.pdu.ExtendedNegotiation; 077import org.dcm4che3.net.pdu.PresentationContext; 078import org.dcm4che3.net.pdu.RoleSelection; 079import org.dcm4che3.net.service.BasicCStoreSCP; 080import org.dcm4che3.net.service.DicomServiceException; 081import org.dcm4che3.net.service.DicomServiceRegistry; 082import org.dcm4che3.tool.common.CLIUtils; 083import org.dcm4che3.util.SafeClose; 084import org.dcm4che3.util.StringUtils; 085import org.slf4j.Logger; 086import org.slf4j.LoggerFactory; 087 088/** 089 * @author Gunter Zeilinger <gunterze@gmail.com> 090 * 091 */ 092public class GetSCU { 093 094 private static final Logger LOG = LoggerFactory.getLogger(GetSCU.class); 095 096 public static enum InformationModel { 097 PatientRoot(UID.PatientRootQueryRetrieveInformationModelGET, "STUDY"), 098 StudyRoot(UID.StudyRootQueryRetrieveInformationModelGET, "STUDY"), 099 PatientStudyOnly(UID.PatientStudyOnlyQueryRetrieveInformationModelGETRetired, "STUDY"), 100 CompositeInstanceRoot(UID.CompositeInstanceRootRetrieveGET, "IMAGE"), 101 WithoutBulkData(UID.CompositeInstanceRetrieveWithoutBulkDataGET, "IMAGE"), 102 HangingProtocol(UID.HangingProtocolInformationModelGET, null), 103 ColorPalette(UID.ColorPaletteQueryRetrieveInformationModelGET, null); 104 105 private final String cuid; 106 final String level; 107 108 InformationModel(String cuid, String level) { 109 this.cuid = cuid; 110 this.level = level; 111 } 112 113 public String getCuid() { 114 return cuid; 115 } 116 } 117 118 private static ResourceBundle rb = 119 ResourceBundle.getBundle("org.dcm4che3.tool.getscu.messages"); 120 121 private static final int[] DEF_IN_FILTER = { 122 Tag.SOPInstanceUID, 123 Tag.StudyInstanceUID, 124 Tag.SeriesInstanceUID 125 }; 126 127 private Device device = new Device("getscu"); 128 private final ApplicationEntity ae; 129 private final Connection conn = new Connection(); 130 private final Connection remote = new Connection(); 131 private final AAssociateRQ rq = new AAssociateRQ(); 132 private int priority; 133 private InformationModel model; 134 private File storageDir; 135 private Attributes keys = new Attributes(); 136 private int[] inFilter = DEF_IN_FILTER; 137 private Association as; 138 139 private BasicCStoreSCP storageSCP = new BasicCStoreSCP("*") { 140 141 @Override 142 protected void store(Association as, PresentationContext pc, Attributes rq, 143 PDVInputStream data, Attributes rsp) 144 throws IOException { 145 if (storageDir == null) 146 return; 147 148 String iuid = rq.getString(Tag.AffectedSOPInstanceUID); 149 String cuid = rq.getString(Tag.AffectedSOPClassUID); 150 String tsuid = pc.getTransferSyntax(); 151 File file = new File(storageDir, iuid ); 152 try { 153 storeTo(as, as.createFileMetaInformation(iuid, cuid, tsuid), 154 data, file); 155 } catch (Exception e) { 156 throw new DicomServiceException(Status.ProcessingFailure, e); 157 } 158 159 } 160 161 162 }; 163 164 public GetSCU() throws IOException { 165 ae = new ApplicationEntity("GETSCU"); 166 device.addConnection(conn); 167 device.addApplicationEntity(ae); 168 ae.addConnection(conn); 169 device.setDimseRQHandler(createServiceRegistry()); 170 } 171 172 public GetSCU(ApplicationEntity appEntity) { 173 this.ae = appEntity; 174 this.device = this.ae.getDevice(); 175 } 176 177 public ApplicationEntity getApplicationEntity() { 178 return ae; 179 } 180 181 public Connection getRemoteConnection() { 182 return remote; 183 } 184 185 public AAssociateRQ getAAssociateRQ() { 186 return rq; 187 } 188 189 public Association getAssociation() { 190 return as; 191 } 192 193 public Device getDevice() { 194 return device; 195 } 196 197 public Attributes getKeys() { 198 return keys; 199 } 200 201 public static void storeTo(Association as, Attributes fmi, 202 PDVInputStream data, File file) throws IOException { 203 LOG.info("{}: M-WRITE {}", as, file); 204 file.getParentFile().mkdirs(); 205 DicomOutputStream out = new DicomOutputStream(file); 206 try { 207 out.writeFileMetaInformation(fmi); 208 data.copyTo(out); 209 } finally { 210 SafeClose.close(out); 211 } 212 } 213 214 private DicomServiceRegistry createServiceRegistry() { 215 DicomServiceRegistry serviceRegistry = new DicomServiceRegistry(); 216 serviceRegistry.addDicomService(storageSCP); 217 return serviceRegistry; 218 } 219 220 public void setStorageDirectory(File storageDir) { 221 if (storageDir != null) 222 if (storageDir.mkdirs()) 223 System.out.println("M-WRITE " + storageDir); 224 this.storageDir = storageDir; 225 } 226 227 public final void setPriority(int priority) { 228 this.priority = priority; 229 } 230 231 public final void setInformationModel(InformationModel model, String[] tss, 232 boolean relational) { 233 this.model = model; 234 rq.addPresentationContext(new PresentationContext(1, model.getCuid(), tss)); 235 if (relational) 236 rq.addExtendedNegotiation(new ExtendedNegotiation(model.getCuid(), 237 QueryOption.toExtendedNegotiationInformation(EnumSet.of(QueryOption.RELATIONAL)))); 238 if (model.level != null) 239 addLevel(model.level); 240 } 241 242 public void addLevel(String s) { 243 keys.setString(Tag.QueryRetrieveLevel, VR.CS, s); 244 } 245 246 public void addKey(int tag, String... ss) { 247 VR vr = ElementDictionary.vrOf(tag, keys.getPrivateCreator(tag)); 248 keys.setString(tag, vr, ss); 249 } 250 251 public final void setInputFilter(int[] inFilter) { 252 this.inFilter = inFilter; 253 } 254 255 private static CommandLine parseComandLine(String[] args) 256 throws ParseException { 257 Options opts = new Options(); 258 addServiceClassOptions(opts); 259 addKeyOptions(opts); 260 addRetrieveLevelOption(opts); 261 addStorageDirectoryOptions(opts); 262 CLIUtils.addConnectOption(opts); 263 CLIUtils.addBindOption(opts, "GETSCU"); 264 CLIUtils.addAEOptions(opts); 265 CLIUtils.addRetrieveTimeoutOption(opts); 266 CLIUtils.addPriorityOption(opts); 267 CLIUtils.addCommonOptions(opts); 268 return CLIUtils.parseComandLine(args, opts, rb, GetSCU.class); 269 } 270 271 @SuppressWarnings("static-access") 272 private static void addRetrieveLevelOption(Options opts) { 273 opts.addOption(OptionBuilder 274 .hasArg() 275 .withArgName("PATIENT|STUDY|SERIES|IMAGE|FRAME") 276 .withDescription(rb.getString("level")) 277 .create("L")); 278 } 279 280 @SuppressWarnings("static-access") 281 private static void addStorageDirectoryOptions(Options opts) { 282 opts.addOption(null, "ignore", false, 283 rb.getString("ignore")); 284 opts.addOption(OptionBuilder 285 .hasArg() 286 .withArgName("path") 287 .withDescription(rb.getString("directory")) 288 .withLongOpt("directory") 289 .create(null)); 290 } 291 292 @SuppressWarnings("static-access") 293 private static void addKeyOptions(Options opts) { 294 opts.addOption(OptionBuilder 295 .hasArgs() 296 .withArgName("attr=value") 297 .withValueSeparator('=') 298 .withDescription(rb.getString("match")) 299 .create("m")); 300 opts.addOption(OptionBuilder 301 .hasArgs() 302 .withArgName("attr") 303 .withDescription(rb.getString("in-attr")) 304 .create("i")); 305 } 306 307 @SuppressWarnings("static-access") 308 private static void addServiceClassOptions(Options opts) { 309 opts.addOption(OptionBuilder 310 .hasArg() 311 .withArgName("name") 312 .withDescription(rb.getString("model")) 313 .create("M")); 314 opts.addOption(null, "relational", false, rb.getString("relational")); 315 CLIUtils.addTransferSyntaxOptions(opts); 316 opts.addOption(OptionBuilder 317 .hasArg() 318 .withArgName("cuid:tsuid[(,|;)...]") 319 .withDescription(rb.getString("store-tc")) 320 .withLongOpt("store-tc") 321 .create()); 322 opts.addOption(OptionBuilder 323 .hasArg() 324 .withArgName("file|url") 325 .withDescription(rb.getString("store-tcs")) 326 .withLongOpt("store-tcs") 327 .create()); 328 } 329 330 331 @SuppressWarnings("unchecked") 332 public static void main(String[] args) { 333 try { 334 CommandLine cl = parseComandLine(args); 335 GetSCU main = new GetSCU(); 336 CLIUtils.configureConnect(main.remote, main.rq, cl); 337 CLIUtils.configureBind(main.conn, main.ae, cl); 338 CLIUtils.configure(main.conn, cl); 339 main.remote.setTlsProtocols(main.conn.tlsProtocols()); 340 main.remote.setTlsCipherSuites(main.conn.getTlsCipherSuites()); 341 configureServiceClass(main, cl); 342 configureKeys(main, cl); 343 main.setPriority(CLIUtils.priorityOf(cl)); 344 configureStorageDirectory(main, cl); 345 ExecutorService executorService = 346 Executors.newSingleThreadExecutor(); 347 ScheduledExecutorService scheduledExecutorService = 348 Executors.newSingleThreadScheduledExecutor(); 349 main.device.setExecutor(executorService); 350 main.device.setScheduledExecutor(scheduledExecutorService); 351 try { 352 main.open(); 353 List<String> argList = cl.getArgList(); 354 if (argList.isEmpty()) 355 main.retrieve(); 356 else 357 for (String arg : argList) 358 main.retrieve(new File(arg)); 359 } finally { 360 main.close(); 361 executorService.shutdown(); 362 scheduledExecutorService.shutdown(); 363 } 364 } catch (ParseException e) { 365 System.err.println("getscu: " + e.getMessage()); 366 System.err.println(rb.getString("try")); 367 System.exit(2); 368 } catch (Exception e) { 369 System.err.println("getscu: " + e.getMessage()); 370 e.printStackTrace(); 371 System.exit(2); 372 } 373 } 374 375 private static void configureServiceClass(GetSCU main, CommandLine cl) 376 throws Exception { 377 main.setInformationModel(informationModelOf(cl), 378 CLIUtils.transferSyntaxesOf(cl), cl.hasOption("relational")); 379 String[] pcs = cl.getOptionValues("store-tc"); 380 if (pcs != null) 381 for (String pc : pcs) { 382 String[] ss = StringUtils.split(pc, ':'); 383 configureStorageSOPClass(main, ss[0], ss[1]); 384 } 385 String[] files = cl.getOptionValues("store-tcs"); 386 if (pcs == null && files == null) 387 files = new String[] { "resource:store-tcs.properties" }; 388 if (files != null) 389 for (String file : files) { 390 Properties p = CLIUtils.loadProperties(file, null); 391 Set<Entry<Object, Object>> entrySet = p.entrySet(); 392 for (Entry<Object, Object> entry : entrySet) 393 configureStorageSOPClass(main, (String) entry.getKey(), (String) entry.getValue()); 394 } 395 } 396 397 private static void configureStorageSOPClass(GetSCU main, String cuid, String tsuids0) { 398 String[] tsuids1 = StringUtils.split(tsuids0, ';'); 399 for (String tsuids2 : tsuids1) { 400 main.addOfferedStorageSOPClass(CLIUtils.toUID(cuid), CLIUtils.toUID(tsuids2)); 401 } 402 } 403 404 public void addOfferedStorageSOPClass(String cuid, String... tsuids) { 405 if (!rq.containsPresentationContextFor(cuid)) 406 rq.addRoleSelection(new RoleSelection(cuid, false, true)); 407 rq.addPresentationContext(new PresentationContext( 408 2 * rq.getNumberOfPresentationContexts() + 1, cuid, tsuids)); 409 } 410 411 private static void configureStorageDirectory(GetSCU main, CommandLine cl) { 412 if (!cl.hasOption("ignore")) { 413 main.setStorageDirectory( 414 new File(cl.getOptionValue("directory", "."))); 415 } 416 } 417 418 private static void configureKeys(GetSCU main, CommandLine cl) { 419 if (cl.hasOption("m")) { 420 String[] keys = cl.getOptionValues("m"); 421 for (int i = 1; i < keys.length; i++, i++) 422 main.addKey(CLIUtils.toTag(keys[i - 1]), StringUtils.split(keys[i], '/')); 423 } 424 if (cl.hasOption("L")) 425 main.addLevel(cl.getOptionValue("L")); 426 if (cl.hasOption("i")) 427 main.setInputFilter(CLIUtils.toTags(cl.getOptionValues("i"))); 428 } 429 430 private static InformationModel informationModelOf(CommandLine cl) throws ParseException { 431 try { 432 return cl.hasOption("M") 433 ? InformationModel.valueOf(cl.getOptionValue("M")) 434 : InformationModel.StudyRoot; 435 } catch(IllegalArgumentException e) { 436 throw new ParseException( 437 MessageFormat.format( 438 rb.getString("invalid-model-name"), 439 cl.getOptionValue("M"))); 440 } 441 } 442 443 public void open() throws IOException, InterruptedException, IncompatibleConnectionException, GeneralSecurityException { 444 as = ae.connect(remote, rq); 445 } 446 447 public void close() throws IOException, InterruptedException { 448 if (as != null && as.isReadyForDataTransfer()) { 449 as.waitForOutstandingRSP(); 450 as.release(); 451 } 452 } 453 454 public void retrieve(File f) throws IOException, InterruptedException { 455 Attributes attrs = new Attributes(); 456 DicomInputStream dis = null; 457 try { 458 dis = new DicomInputStream(f); 459 attrs.addSelected(dis.readDataset(-1, -1), inFilter); 460 } finally { 461 SafeClose.close(dis); 462 } 463 attrs.addAll(keys); 464 retrieve(attrs); 465 } 466 467 public void retrieve() throws IOException, InterruptedException { 468 retrieve(keys); 469 } 470 471 private void retrieve(Attributes keys) throws IOException, InterruptedException { 472 DimseRSPHandler rspHandler = new DimseRSPHandler(as.nextMessageID()) { 473 474 @Override 475 public void onDimseRSP(Association as, Attributes cmd, 476 Attributes data) { 477 super.onDimseRSP(as, cmd, data); 478 } 479 }; 480 481 retrieve (keys, rspHandler); 482 } 483 484 public void retrieve(DimseRSPHandler rspHandler) throws IOException, InterruptedException { 485 retrieve(keys, rspHandler); 486 } 487 488 private void retrieve(Attributes keys, DimseRSPHandler rspHandler) throws IOException, InterruptedException { 489 as.cget(model.getCuid(), priority, keys, null, rspHandler); 490 } 491 492}