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 ***** */ 038package org.dcm4che3.tool.wadors; 039 040import java.io.BufferedReader; 041import java.io.ByteArrayInputStream; 042import java.io.ByteArrayOutputStream; 043import java.io.File; 044import java.io.FileInputStream; 045import java.io.FileNotFoundException; 046import java.io.IOException; 047import java.io.InputStream; 048import java.io.InputStreamReader; 049import java.io.PrintWriter; 050import java.lang.reflect.Field; 051import java.net.HttpURLConnection; 052import java.net.URL; 053import java.nio.file.Files; 054import java.nio.file.Path; 055import java.nio.file.StandardCopyOption; 056import java.util.Arrays; 057import java.util.HashMap; 058import java.util.List; 059import java.util.Map; 060import java.util.ResourceBundle; 061 062import javax.xml.transform.OutputKeys; 063import javax.xml.transform.Templates; 064import javax.xml.transform.TransformerFactory; 065import javax.xml.transform.sax.SAXTransformerFactory; 066import javax.xml.transform.sax.TransformerHandler; 067import javax.xml.transform.stream.StreamResult; 068import javax.xml.transform.stream.StreamSource; 069 070import org.apache.commons.cli.CommandLine; 071import org.apache.commons.cli.OptionBuilder; 072import org.apache.commons.cli.Options; 073import org.apache.commons.cli.ParseException; 074import org.dcm4che3.data.Attributes; 075import org.dcm4che3.data.Tag; 076import org.dcm4che3.io.DicomInputStream; 077import org.dcm4che3.io.DicomOutputStream; 078import org.dcm4che3.io.SAXReader; 079import org.dcm4che3.io.SAXWriter; 080import org.dcm4che3.mime.MultipartInputStream; 081import org.dcm4che3.mime.MultipartParser; 082import org.dcm4che3.tool.common.CLIUtils; 083import org.dcm4che3.tool.common.SimpleHTTPResponse; 084import org.dcm4che3.tool.wadors.test.WadoRSResponse; 085import org.dcm4che3.ws.rs.MediaTypes; 086import org.slf4j.Logger; 087import org.slf4j.LoggerFactory; 088 089/** 090 * WADO-RS client tool. 091 * 092 * @see <a href= 093 * "http://medical.nema.org/medical/dicom/current/output/html/part18.html#sect_6.5"> 094 * DICOM PS3.18 (Web Services) WADO-RS</a> 095 * 096 * @author Hesham Elbadawi <bsdreko@gmail.com> 097 * @author Hermann Czedik-Eysenberg <hermann-agfa@czedik.net> 098 */ 099public class WadoRS { 100 101 private static final Logger LOG = LoggerFactory.getLogger(WadoRS.class); 102 103 private static byte[] copyHolder; 104 105 private static Options options; 106 107 private static ResourceBundle rb = ResourceBundle 108 .getBundle("org.dcm4che3.tool.wadors.messages"); 109 110 private SAXTransformerFactory saxTransformer; 111 112 private Templates xsltTemplates; 113 114 private File outDir; 115 116 private File xsltFile; 117 118 private String requestTimeOut; 119 120 private boolean xmlIndent = false; 121 122 private boolean xmlIncludeKeyword = true; 123 124 private boolean isMetadata = false; 125 126 private String[] acceptTypes; 127 128 private String url; 129 130 private ResponseWriter writerType; 131 132 private int partIndex = 0; 133 134 private boolean dumpHeader = false; 135 136 private final Map<String, Path> retrievedInstances = new HashMap<String, Path>(); 137 138 private WadoRSResponse response; 139 140 public WadoRS() { 141 } 142 143 public WadoRS(String url, File retrieveDir) { 144 this.outDir = retrieveDir; 145 this.url = url; 146 } 147 148 @SuppressWarnings("static-access") 149 private static CommandLine parseComandLine(String[] args) 150 throws ParseException { 151 options = new Options(); 152 153 options.addOption(null, "request-timeout", true, 154 rb.getString("request-timeout")); 155 156 options.addOption(OptionBuilder.withArgName("mediaType;ts") 157 .withLongOpt("accept-type") 158 .withDescription(rb.getString("accept-type")).hasArg(true) 159 .create()); 160 161 addOutputOptions(options); 162 163 CLIUtils.addCommonOptions(options); 164 165 return CLIUtils.parseComandLine(args, options, rb, WadoRS.class); 166 } 167 168 @SuppressWarnings("static-access") 169 private static void addOutputOptions(Options opts) { 170 opts.addOption(OptionBuilder.withLongOpt("out-dir").hasArg() 171 .withArgName("directory") 172 .withDescription(rb.getString("out-dir")).create()); 173 opts.addOption(OptionBuilder.withLongOpt("dump-headers").hasArg(false) 174 .withDescription(rb.getString("dump-headers")).create()); 175 opts.addOption(OptionBuilder.withLongOpt("xsl").hasArg() 176 .withArgName("xsl-file").withDescription(rb.getString("xsl")) 177 .create("x")); 178 opts.addOption("I", "indent", false, rb.getString("indent")); 179 opts.addOption("K", "no-keyword", false, rb.getString("no-keyword")); 180 } 181 182 public static void main(String[] args) { 183 CommandLine cl = null; 184 try { 185 WadoRS main = new WadoRS(); 186 cl = parseComandLine(args); 187 188 if (cl.hasOption("out-dir")) 189 main.setOutDir(new File(cl.getOptionValue("out-dir"))); 190 191 if (cl.hasOption("x")) 192 main.setXsltFile(new File(cl.getOptionValue("x"))); 193 194 main.setXmlIndent(cl.hasOption("I")); 195 196 main.setXmlIncludeKeyword(!cl.hasOption("K")); 197 198 if (cl.hasOption("request-timeout")) 199 main.setRequestTimeOut(cl.getOptionValue("request-timout")); 200 201 if (cl.hasOption("accept-type")) { 202 main.setAcceptType(cl.getOptionValues("accept-type")); 203 } else { 204 System.out.println("wadors: missing required option accept-type"); 205 System.err.println(rb.getString("try")); 206 System.exit(2); 207 } 208 209 if (cl.hasOption("dump-headers")) 210 main.dumpHeader = true; 211 212 main.setUrl(cl.getArgs()[0]); 213 214 main.isMetadata = isMetaDataURL(main.getUrl()); 215 216 String response = null; 217 try { 218 response = sendRequest(main).toString(); 219 } catch (IOException e) { 220 System.out.print("Error sending request {}" + e); 221 } 222 223 System.out.print(response); 224 } catch (ParseException e) { 225 LOG.error("wadors: " + e.getMessage()); 226 System.err.println(rb.getString("try")); 227 System.exit(2); 228 } 229 } 230 231 public void wadors() throws IOException { 232 isMetadata = isMetaDataURL(url); 233 234 sendRequest(this); 235 } 236 237 private static boolean isMetaDataURL(String url) { 238 return url.contains("/metadata"); 239 } 240 241 private static SimpleHTTPResponse sendRequest(final WadoRS main) throws IOException { 242 URL newUrl = new URL(main.getUrl()); 243 244 LOG.info("WADO-RS URL: {}", newUrl); 245 246 HttpURLConnection connection = (HttpURLConnection) newUrl.openConnection(); 247 248 connection.setDoOutput(true); 249 250 connection.setDoInput(true); 251 252 connection.setInstanceFollowRedirects(false); 253 254 connection.setRequestMethod("GET"); 255 256 connection.setRequestProperty("charset", "utf-8"); 257 258 String[] acceptHeaders = compileAcceptHeader(main.acceptTypes); 259 260 LOG.info("Accept-Headers: {}", Arrays.toString(acceptHeaders)); 261 262 for (String acceptStr : acceptHeaders) 263 connection.addRequestProperty("Accept", acceptStr); 264 265 if (main.getRequestTimeOut() != null) { 266 connection.setConnectTimeout(Integer.valueOf(main.getRequestTimeOut())); 267 connection.setReadTimeout(Integer.valueOf(main.getRequestTimeOut())); 268 } 269 270 connection.setUseCaches(false); 271 272 int responseCode = connection.getResponseCode(); 273 String responseMessage = connection.getResponseMessage(); 274 275 boolean isErrorCase = responseCode >= HttpURLConnection.HTTP_BAD_REQUEST; 276 if(!isErrorCase) { 277 278 InputStream in = null; 279 if (connection.getHeaderField("content-type").contains("application/json") || connection.getHeaderField("content-type").contains("application/zip")) { 280 String headerPath; 281 in = connection.getInputStream(); 282 if (main.dumpHeader) 283 headerPath = writeHeader(connection.getHeaderFields(), new File(main.outDir, "out.json" + "-head")); 284 else { 285 headerPath = connection.getHeaderField("content-location"); 286 } 287 File f = new File(main.outDir, connection.getHeaderField("content-type").contains("application/json") ? "out.json" : "out.zip"); 288 Files.copy(in, f.toPath(), StandardCopyOption.REPLACE_EXISTING); 289 main.retrievedInstances.put(headerPath, f.toPath().toAbsolutePath()); 290 } else { 291 if (main.dumpHeader) 292 dumpHeader(main, connection.getHeaderFields()); 293 in = connection.getInputStream(); 294 try { 295 File spool = new File(main.outDir, "Spool"); 296 Files.copy(in, spool.toPath(), StandardCopyOption.REPLACE_EXISTING); 297 String boundary; 298 BufferedReader rdr = new BufferedReader(new InputStreamReader(new FileInputStream(spool))); 299 boundary = (rdr.readLine()); 300 boundary = boundary.substring(2, boundary.length()); 301 rdr.close(); 302 FileInputStream fin = new FileInputStream(spool); 303 new MultipartParser(boundary).parse(fin, new MultipartParser.Handler() { 304 305 @Override 306 public void bodyPart(int partNumber, MultipartInputStream partIn) throws IOException { 307 308 Map<String, List<String>> headerParams = partIn.readHeaderParams(); 309 String mediaType; 310 String contentType = headerParams.get("content-type").get(0); 311 312 if (contentType.contains("transfer-syntax")) 313 mediaType = contentType.split(";")[0]; 314 else 315 mediaType = contentType; 316 317 // choose writer 318 if (main.isMetadata) { 319 main.writerType = ResponseWriter.XML; 320 321 } else { 322 if (mediaType.equalsIgnoreCase("application/dicom")) { 323 main.writerType = ResponseWriter.DICOM; 324 } else if (isBulkMediaType(mediaType)) { 325 main.writerType = ResponseWriter.BULK; 326 } else { 327 throw new IllegalArgumentException("Unknown media type " + "returned by server, media type = " + mediaType); 328 } 329 330 } 331 try { 332 main.writerType.readBody(main, partIn, headerParams); 333 } catch (Exception e) { 334 System.out.println("Error parsing media type to determine extension" + e); 335 } 336 } 337 338 private boolean isBulkMediaType(String mediaType) { 339 if (mediaType.contains("octet-stream")) 340 return true; 341 for (Field field : MediaTypes.class.getFields()) { 342 try { 343 if (field.getType().equals(String.class)) { 344 String tmp = (String) field.get(field); 345 if (tmp.equalsIgnoreCase(mediaType)) 346 return true; 347 } 348 } catch (Exception e) { 349 System.out.println("Error deciding media type " + e); 350 } 351 } 352 353 return false; 354 } 355 }); 356 fin.close(); 357 spool.delete(); 358 } catch (Exception e) { 359 System.out.println("Error parsing Server response - " + e); 360 } 361 } 362 363 } else { 364 LOG.error("Server returned {} - {}", responseCode, responseMessage); 365 } 366 367 connection.disconnect(); 368 369 main.response = new WadoRSResponse(responseCode, responseMessage, main.retrievedInstances); 370 return new SimpleHTTPResponse(responseCode, responseMessage); 371 372 } 373 374 private static String dumpHeader(WadoRS main, Map<String, List<String>> headerFields) throws FileNotFoundException { 375 File f = new File(main.outDir, "request-head"); 376 main.retrievedInstances.put("multipart-request-head", f.toPath().toAbsolutePath()); 377 return writeHeader(headerFields, f); 378 } 379 380 private static String writeHeader(Map<String, List<String>> headerFields, 381 File f) throws FileNotFoundException { 382 PrintWriter writer = new PrintWriter(f); 383 for(String key : headerFields.keySet()) { 384 if(key != null) 385 writer.print(key+":\t"); 386 writer.print(headerFields.get(key)); 387 writer.println(); 388 } 389 writer.close(); 390 return f.getAbsolutePath(); 391 } 392 393 private static String[] compileAcceptHeader(String[] acceptTypes) { 394 String[] acceptHeaders = new String[acceptTypes.length]; 395 for (int i = 0; i < acceptTypes.length; i++) { 396 String acceptType = acceptTypes[i]; 397 if (acceptType.contains("application/json") || acceptType.contains("application/zip") || acceptType.contains("multipart/related")) { 398 acceptHeaders[i] = acceptType; 399 } else { 400 String mediaType = acceptType; 401 String transferSyntaxUID = null; 402 if (mediaType.contains(";")) { 403 String[] splittedAcceptHeader = acceptType.split(";"); 404 mediaType = splittedAcceptHeader[0]; 405 transferSyntaxUID = splittedAcceptHeader[1]; 406 } 407 408 acceptHeaders[i] = "multipart/related; type=" + mediaType; 409 410 if (transferSyntaxUID != null) { 411 acceptHeaders[i] += "; transfer-syntax=" + transferSyntaxUID; 412 } 413 } 414 } 415 return acceptHeaders; 416 } 417 418 private enum ResponseWriter { 419 XML { 420 @Override 421 boolean readBody(WadoRS wadors, InputStream in, Map<String, List<String>> headerParams) 422 throws Exception { 423 TransformerHandler th = getTransformerHandler(wadors); 424 th.getTransformer().setOutputProperty(OutputKeys.INDENT, wadors.xmlIndent ? "yes" : "no"); 425 // here the sax parser would actually close the input stream immediately 426 InputStream is = cloneStream(in); 427 428 Attributes attrs = SAXReader.parse(is); 429 SAXWriter saxWriter = new SAXWriter(th); 430 431 File outputDirectory = wadors.getOutDir(); 432 433 String fileName = attrs.getString(Tag.SOPInstanceUID); 434 if (fileName == null) { 435 fileName = "" + wadors.getNextPartIndex(); 436 LOG.info("Unable to decide SopInstanceUID, using part counter"); 437 } 438 outputDirectory = ensureDirs(wadors, attrs); 439 440 File out = new File(outputDirectory, fileName + ".xml"); 441 th.setResult(new StreamResult(out)); 442 saxWriter.setIncludeKeyword(wadors.xmlIncludeKeyword); 443 saxWriter.write(attrs); 444 String headPath; 445 if (wadors.dumpHeader) { 446 headPath = writeHeader(headerParams, new File(out.getAbsolutePath() + "-head")); 447 } else { 448 headPath = attrs.getString(Tag.SOPInstanceUID); 449 } 450 wadors.retrievedInstances.put(headPath, out.toPath().toAbsolutePath()); 451 return true; 452 } 453 454 private TransformerHandler getTransformerHandler(WadoRS main) 455 throws Exception { 456 457 SAXTransformerFactory stf = main.saxTransformer; 458 459 if (stf == null) 460 main.saxTransformer = stf = (SAXTransformerFactory) TransformerFactory 461 .newInstance(); 462 463 if (main.getXsltFile() == null) 464 return stf.newTransformerHandler(); 465 466 Templates templates = main.xsltTemplates; 467 468 if (templates == null) { 469 templates = stf 470 .newTemplates(new StreamSource(main.xsltFile)); 471 main.xsltTemplates = templates; 472 } 473 474 return stf.newTransformerHandler(templates); 475 } 476 }, 477 DICOM { 478 @Override 479 boolean readBody(WadoRS wadors, InputStream in, Map<String, List<String>> headerParams) 480 throws IOException { 481 Attributes fmi; 482 Attributes attrs; 483 DicomInputStream dis = new DicomInputStream(in); 484 try { 485 fmi = dis.readFileMetaInformation(); 486 attrs = dis.readDataset(-1, -1); 487 } finally { 488 dis.close(); 489 } 490 491 File outputDirectory = wadors.getOutDir(); 492 493 String fileName = attrs.getString(Tag.SOPInstanceUID); 494 if (fileName == null) { 495 fileName = "" + wadors.getNextPartIndex(); 496 LOG.info("Unable to decide SopInstanceUID, using part counter"); 497 } 498 outputDirectory = ensureDirs(wadors, attrs); 499 500 File out = new File(outputDirectory, fileName + ".dcm"); 501 DicomOutputStream os = new DicomOutputStream(out); 502 os.writeDataset(fmi, attrs); 503 os.close(); 504 String headPath; 505 if (wadors.dumpHeader) { 506 headPath = writeHeader(headerParams, new File(out.getAbsolutePath() + "-head")); 507 } else { 508 headPath = attrs.getString(Tag.SOPInstanceUID); 509 } 510 wadors.retrievedInstances.put(headPath, out.toPath().toAbsolutePath()); 511 return true; 512 } 513 }, 514 BULK { 515 @Override 516 boolean readBody(WadoRS wadors, InputStream in, Map<String, List<String>> headerParams) 517 throws IOException { 518 519 String frame = null; 520 List<String> headerContentLocations = headerParams.get("content-location"); 521 String contentLocation = (headerContentLocations != null && !headerContentLocations.isEmpty()) ? headerContentLocations.get(0) : null; 522 if (contentLocation != null && contentLocation.contains("frames/")) 523 frame = contentLocation.split("frames/")[1]; 524 525 File outputDirectory = wadors.getOutDir(); 526 String fileName = "" + wadors.getNextPartIndex(); 527 528 if (frame != null) { 529 fileName += "-frame" + frame; 530 } 531 532 File out = new File(outputDirectory,fileName + ".blk"); 533 Files.copy(in,out.toPath(), 534 StandardCopyOption.REPLACE_EXISTING); 535 String headPath; 536 if(wadors.dumpHeader) { 537 headPath = writeHeader(headerParams, new File(out.getAbsolutePath()+"-head")); 538 } 539 else { 540 headPath = fileName; 541 } 542 wadors.retrievedInstances.put(headPath, out.toPath().toAbsolutePath()); 543 return true; 544 } 545 }; 546 547 abstract boolean readBody(WadoRS wadors, InputStream in, Map<String, List<String>> headerParams) 548 throws IOException, Exception; 549 550 protected File ensureDirs(WadoRS wadors, Attributes attrs) { 551 String url = wadors.getUrl(); 552 String seriesuid,studyuid; 553 File parentDir = wadors.getOutDir(); 554 studyuid = url.split("studies/")[1].split("/")[0]; 555 if(url.contains("series")) { 556 seriesuid = url.split("series/")[1].split("/")[0]; 557 } 558 else { 559 seriesuid = attrs.getString(Tag.SeriesInstanceUID); 560 } 561 File outDir = new File(new File(parentDir,studyuid), seriesuid); 562 if(!outDir.exists()) 563 outDir.mkdirs(); 564 return outDir; 565 } 566 567 private static InputStream cloneStream(InputStream inputStream) { 568 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 569 try { 570 byte[] buf = new byte[4096]; 571 572 // inputStream is your original stream. 573 for (int n; 0 < (n = inputStream.read(buf));) { 574 baos.write(buf, 0, n); 575 } 576 baos.close(); 577 copyHolder = baos.toByteArray(); 578 } catch (Exception e) { 579 System.out 580 .println("wadors : Error copying the stream " + e); 581 } 582 // This is the new stream that you can pass it to other code and 583 // use its data. 584 return new ByteArrayInputStream(copyHolder); 585 } 586 } 587 588 public File getOutDir() { 589 return outDir; 590 } 591 592 public void setOutDir(File outDir) { 593 outDir.mkdirs(); 594 this.outDir = outDir; 595 } 596 597 public File getXsltFile() { 598 return xsltFile; 599 } 600 601 public void setXsltFile(File xsltFile) { 602 this.xsltFile = xsltFile; 603 } 604 605 public String getRequestTimeOut() { 606 return requestTimeOut; 607 } 608 609 public void setRequestTimeOut(String requestTimeOut) { 610 this.requestTimeOut = requestTimeOut; 611 } 612 613 public boolean isXmlIndent() { 614 return xmlIndent; 615 } 616 617 public void setXmlIndent(boolean xmlIndent) { 618 this.xmlIndent = xmlIndent; 619 } 620 621 public boolean isXmlIncludeKeyword() { 622 return xmlIncludeKeyword; 623 } 624 625 public void setXmlIncludeKeyword(boolean xmlIncludeKeyword) { 626 this.xmlIncludeKeyword = xmlIncludeKeyword; 627 } 628 629 public String getUrl() { 630 return url; 631 } 632 633 public void setUrl(String url) { 634 this.url = url; 635 } 636 637 public String[] getAcceptType() { 638 return acceptTypes; 639 } 640 641 public void setAcceptType(String[] acceptTypes) { 642 this.acceptTypes = acceptTypes; 643 } 644 645 public int getNextPartIndex() { 646 return ++this.partIndex; 647 } 648 649 public Map<String, Path> getRetrievedInstances() { 650 return retrievedInstances; 651 } 652 653 public WadoRSResponse getResponse() { 654 return response; 655 } 656 657 public void setDumpHeaders(boolean b) { 658 this.dumpHeader = b; 659 } 660}