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.movescu; 040 041import java.io.File; 042import java.io.IOException; 043import java.security.GeneralSecurityException; 044import java.text.MessageFormat; 045import java.util.EnumSet; 046import java.util.List; 047import java.util.ResourceBundle; 048import java.util.concurrent.ExecutorService; 049import java.util.concurrent.Executors; 050import java.util.concurrent.ScheduledExecutorService; 051 052import org.apache.commons.cli.CommandLine; 053import org.apache.commons.cli.OptionBuilder; 054import org.apache.commons.cli.Options; 055import org.apache.commons.cli.ParseException; 056import org.dcm4che3.data.Tag; 057import org.dcm4che3.data.UID; 058import org.dcm4che3.data.Attributes; 059import org.dcm4che3.data.ElementDictionary; 060import org.dcm4che3.data.VR; 061import org.dcm4che3.io.DicomInputStream; 062import org.dcm4che3.net.ApplicationEntity; 063import org.dcm4che3.net.Association; 064import org.dcm4che3.net.Connection; 065import org.dcm4che3.net.Device; 066import org.dcm4che3.net.DimseRSPHandler; 067import org.dcm4che3.net.IncompatibleConnectionException; 068import org.dcm4che3.net.QueryOption; 069import org.dcm4che3.net.Status; 070import org.dcm4che3.net.pdu.AAssociateRQ; 071import org.dcm4che3.net.pdu.ExtendedNegotiation; 072import org.dcm4che3.net.pdu.PresentationContext; 073import org.dcm4che3.tool.common.CLIUtils; 074import org.dcm4che3.util.SafeClose; 075import org.dcm4che3.util.StringUtils; 076import org.slf4j.Logger; 077import org.slf4j.LoggerFactory; 078 079/** 080 * @author Gunter Zeilinger <gunterze@gmail.com> 081 * 082 */ 083public class MoveSCU { 084 085 public static enum InformationModel { 086 PatientRoot(UID.PatientRootQueryRetrieveInformationModelMOVE, "STUDY"), 087 StudyRoot(UID.StudyRootQueryRetrieveInformationModelMOVE, "STUDY"), 088 PatientStudyOnly(UID.PatientStudyOnlyQueryRetrieveInformationModelMOVERetired, "STUDY"), 089 CompositeInstanceRoot(UID.CompositeInstanceRootRetrieveMOVE, "IMAGE"), 090 HangingProtocol(UID.HangingProtocolInformationModelMOVE, null), 091 ColorPalette(UID.ColorPaletteQueryRetrieveInformationModelMOVE, null); 092 093 final String cuid; 094 final String level; 095 096 InformationModel(String cuid, String level) { 097 this.cuid = cuid; 098 this.level = level; 099 } 100 public String getCuid() { 101 return cuid; 102 } 103 104 } 105 106 private static ResourceBundle rb = 107 ResourceBundle.getBundle("org.dcm4che3.tool.movescu.messages"); 108 109 private static final int[] DEF_IN_FILTER = { 110 Tag.SOPInstanceUID, 111 Tag.StudyInstanceUID, 112 Tag.SeriesInstanceUID 113 }; 114 115 private static final Logger LOG = LoggerFactory.getLogger(MoveSCU.class); 116 117 private ApplicationEntity ae = new ApplicationEntity("MOVESCU"); 118 private final Connection conn = new Connection(); 119 private final Connection remote = new Connection(); 120 private final AAssociateRQ rq = new AAssociateRQ(); 121 private Device device; 122 private int priority; 123 private String destination; 124 private InformationModel model; 125 private Attributes keys = new Attributes(); 126 private int[] inFilter = DEF_IN_FILTER; 127 private Association as; 128 private int idleRetrieveTimeout; 129 private int exitCode; 130 131 public MoveSCU() throws IOException { 132 this.device = new Device("movescu"); 133 this.device.addConnection(conn); 134 this.device.addApplicationEntity(ae); 135 ae.addConnection(conn); 136 } 137 138 public MoveSCU(ApplicationEntity ae) { 139 this.ae = ae; 140 this.device = ae.getDevice(); 141 } 142 143 public final void setPriority(int priority) { 144 this.priority = priority; 145 } 146 147 public final void setInformationModel(InformationModel model, String[] tss, 148 boolean relational) { 149 this.model = model; 150 rq.addPresentationContext(new PresentationContext(1, model.cuid, tss)); 151 if (relational) 152 rq.addExtendedNegotiation(new ExtendedNegotiation(model.cuid, 153 QueryOption.toExtendedNegotiationInformation(EnumSet.of(QueryOption.RELATIONAL)))); 154 if (model.level != null) 155 addLevel(model.level); 156 } 157 158 public ApplicationEntity getApplicationEntity() { 159 return ae; 160 } 161 162 public Connection getRemoteConnection() { 163 return remote; 164 } 165 166 public AAssociateRQ getAAssociateRQ() { 167 return rq; 168 } 169 170 public Association getAssociation() { 171 return as; 172 } 173 174 public Device getDevice() { 175 return device; 176 } 177 178 public Attributes getKeys() { 179 return keys; 180 } 181 public void addLevel(String s) { 182 keys.setString(Tag.QueryRetrieveLevel, VR.CS, s); 183 } 184 185 public final void setDestination(String destination) { 186 this.destination = destination; 187 } 188 189 public void addKey(int tag, String... ss) { 190 VR vr = ElementDictionary.vrOf(tag, keys.getPrivateCreator(tag)); 191 keys.setString(tag, vr, ss); 192 } 193 194 public final void setInputFilter(int[] inFilter) { 195 this.inFilter = inFilter; 196 } 197 198 private static CommandLine parseComandLine(String[] args) 199 throws ParseException { 200 Options opts = new Options(); 201 addServiceClassOptions(opts); 202 addKeyOptions(opts); 203 addRetrieveLevelOption(opts); 204 addDestinationOption(opts); 205 addTimeoutOption(opts); 206 CLIUtils.addConnectOption(opts); 207 CLIUtils.addBindOption(opts, "MOVESCU"); 208 CLIUtils.addAEOptions(opts); 209 CLIUtils.addRetrieveTimeoutOption(opts); 210 CLIUtils.addPriorityOption(opts); 211 CLIUtils.addCommonOptions(opts); 212 return CLIUtils.parseComandLine(args, opts, rb, MoveSCU.class); 213 } 214 215 @SuppressWarnings("static-access") 216 private static void addRetrieveLevelOption(Options opts) { 217 opts.addOption(OptionBuilder 218 .hasArg() 219 .withArgName("PATIENT|STUDY|SERIES|IMAGE|FRAME") 220 .withDescription(rb.getString("level")) 221 .create("L")); 222 } 223 224 @SuppressWarnings("static-access") 225 private static void addDestinationOption(Options opts) { 226 opts.addOption(OptionBuilder 227 .withLongOpt("dest") 228 .hasArg() 229 .withArgName("aet") 230 .withDescription(rb.getString("dest")) 231 .create()); 232 233 } 234 235 @SuppressWarnings("static-access") 236 private static void addTimeoutOption(Options opts) { 237 opts.addOption(OptionBuilder 238 .hasArg() 239 .withArgName("ms") 240 .withDescription(rb.getString("idle-retrieve-timeout")) 241 .withLongOpt("idle-retrieve-timeout") 242 .create(null)); 243 } 244 245 @SuppressWarnings("static-access") 246 private static void addKeyOptions(Options opts) { 247 opts.addOption(OptionBuilder 248 .hasArgs() 249 .withArgName("attr=value") 250 .withValueSeparator('=') 251 .withDescription(rb.getString("match")) 252 .create("m")); 253 opts.addOption(OptionBuilder 254 .hasArgs() 255 .withArgName("attr") 256 .withDescription(rb.getString("in-attr")) 257 .create("i")); 258 } 259 260 @SuppressWarnings("static-access") 261 private static void addServiceClassOptions(Options opts) { 262 opts.addOption(OptionBuilder 263 .hasArg() 264 .withArgName("name") 265 .withDescription(rb.getString("model")) 266 .create("M")); 267 CLIUtils.addTransferSyntaxOptions(opts); 268 opts.addOption(null, "relational", false, rb.getString("relational")); 269 } 270 271 @SuppressWarnings("unchecked") 272 public static void main(String[] args) { 273 try { 274 CommandLine cl = parseComandLine(args); 275 MoveSCU main = new MoveSCU(); 276 CLIUtils.configureConnect(main.remote, main.rq, cl); 277 CLIUtils.configureBind(main.conn, main.ae, cl); 278 CLIUtils.configure(main.conn, cl); 279 main.remote.setTlsProtocols(main.conn.tlsProtocols()); 280 main.remote.setTlsCipherSuites(main.conn.getTlsCipherSuites()); 281 configureServiceClass(main, cl); 282 configureKeys(main, cl); 283 main.setPriority(CLIUtils.priorityOf(cl)); 284 main.setDestination(destinationOf(cl)); 285 main.idleRetrieveTimeout = CLIUtils.getIntOption(cl, "idle-retrieve-timeout", -1); 286 ExecutorService executorService = 287 Executors.newSingleThreadExecutor(); 288 ScheduledExecutorService scheduledExecutorService = 289 Executors.newSingleThreadScheduledExecutor(); 290 main.device.setExecutor(executorService); 291 main.device.setScheduledExecutor(scheduledExecutorService); 292 try { 293 main.open(); 294 List<String> argList = cl.getArgList(); 295 if (argList.isEmpty()) 296 main.retrieve(); 297 else 298 for (String arg : argList) 299 main.retrieve(new File(arg)); 300 } finally { 301 main.close(); 302 executorService.shutdown(); 303 scheduledExecutorService.shutdown(); 304 } 305 System.exit(main.exitCode); 306 } catch (ParseException e) { 307 System.err.println("movescu: " + e.getMessage()); 308 System.err.println(rb.getString("try")); 309 System.exit(2); 310 } catch (Exception e) { 311 System.err.println("movescu: " + e.getMessage()); 312 e.printStackTrace(); 313 System.exit(2); 314 } 315 } 316 317 private static void configureServiceClass(MoveSCU main, CommandLine cl) throws ParseException { 318 main.setInformationModel(informationModelOf(cl), 319 CLIUtils.transferSyntaxesOf(cl), cl.hasOption("relational")); 320 } 321 322 private static String destinationOf(CommandLine cl) throws ParseException { 323 if (cl.hasOption("dest")) 324 return cl.getOptionValue("dest"); 325 throw new ParseException(rb.getString("missing-dest")); 326 } 327 328 private static void configureKeys(MoveSCU main, CommandLine cl) { 329 if (cl.hasOption("m")) { 330 String[] keys = cl.getOptionValues("m"); 331 for (int i = 1; i < keys.length; i++, i++) 332 main.addKey(CLIUtils.toTag(keys[i - 1]), StringUtils.split(keys[i], '/')); 333 } 334 if (cl.hasOption("L")) 335 main.addLevel(cl.getOptionValue("L")); 336 if (cl.hasOption("i")) 337 main.setInputFilter(CLIUtils.toTags(cl.getOptionValues("i"))); 338 } 339 340 private static InformationModel informationModelOf(CommandLine cl) throws ParseException { 341 try { 342 return cl.hasOption("M") 343 ? InformationModel.valueOf(cl.getOptionValue("M")) 344 : InformationModel.StudyRoot; 345 } catch(IllegalArgumentException e) { 346 throw new ParseException(MessageFormat.format( 347 rb.getString("invalid-model-name"), 348 cl.getOptionValue("M"))); 349 } 350 } 351 352 public void open() throws IOException, InterruptedException, 353 IncompatibleConnectionException, GeneralSecurityException { 354 as = ae.connect(remote, rq); 355 } 356 357 public void close() throws IOException, InterruptedException { 358 if (as != null && as.isReadyForDataTransfer()) { 359 as.waitForOutstandingRSP(); 360 as.release(); 361 } 362 } 363 364 public void retrieve(File f) throws IOException, InterruptedException { 365 Attributes attrs = new Attributes(); 366 DicomInputStream dis = null; 367 try { 368 attrs.addSelected(new DicomInputStream(f).readDataset(-1, -1), inFilter); 369 } finally { 370 SafeClose.close(dis); 371 } 372 attrs.addAll(keys); 373 retrieve(attrs); 374 } 375 376 public void retrieve() throws IOException, InterruptedException { 377 retrieve(keys); 378 } 379 380 private void retrieve(Attributes keys) throws IOException, InterruptedException { 381 DimseRSPHandler rspHandler = new DimseRSPHandler(as.nextMessageID()) { 382 int lastRemaining = -1; 383 long lastChanged; 384 @Override 385 public void onDimseRSP(Association as, Attributes cmd, 386 Attributes data) { 387 super.onDimseRSP(as, cmd, data); 388 if (idleRetrieveTimeout != -1 && Status.isPending(cmd.getInt(Tag.Status, -1))) { 389 int remaining = cmd.getInt(Tag.NumberOfRemainingSuboperations, -1); 390 if(remaining > 0) { 391 if(lastRemaining != remaining) { 392 lastRemaining = remaining; 393 lastChanged = System.currentTimeMillis(); 394 } else { 395 long idleTime = System.currentTimeMillis()-lastChanged; 396 if (idleTime > idleRetrieveTimeout){ 397 LOG.warn("Cancel C-MOVE request after "+idleTime+"ms of idle time! response:"+cmd); 398 try { 399 exitCode = 3; 400 cancel(as); 401 } catch (IOException e) { 402 e.printStackTrace(); 403 } 404 } else { 405 LOG.info("C_MOVE Request is idle for "+idleTime+"ms! idleRetrieveTimeout="+idleRetrieveTimeout); 406 } 407 } 408 } 409 } 410 } 411 }; 412 413 as.cmove(model.cuid, priority, keys, null, destination, rspHandler); 414 } 415 416 public void retrieve(Attributes keys, DimseRSPHandler handler) throws IOException, InterruptedException { 417 as.cmove(model.cuid, priority, keys, null, destination, handler); 418 } 419 420 public void setLevel(InformationModel mdl) { 421 this.model = mdl; 422 if(mdl.level.equalsIgnoreCase("IMAGE")) { 423 this.rq.addExtendedNegotiation(new ExtendedNegotiation(model.cuid, new byte[]{1})); 424 } 425 } 426}