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.qidors; 039 040 041import java.io.BufferedReader; 042import java.io.ByteArrayInputStream; 043import java.io.File; 044import java.io.IOException; 045import java.io.InputStream; 046import java.io.InputStreamReader; 047import java.io.UnsupportedEncodingException; 048import java.net.HttpURLConnection; 049import java.net.URL; 050import java.net.URLEncoder; 051import java.nio.file.Files; 052import java.nio.file.StandardCopyOption; 053import java.util.ArrayList; 054import java.util.HashMap; 055import java.util.List; 056import java.util.Properties; 057import java.util.ResourceBundle; 058 059import javax.json.Json; 060import javax.xml.parsers.ParserConfigurationException; 061import javax.xml.transform.OutputKeys; 062import javax.xml.transform.Templates; 063import javax.xml.transform.TransformerFactory; 064import javax.xml.transform.sax.SAXTransformerFactory; 065import javax.xml.transform.sax.TransformerHandler; 066import javax.xml.transform.stream.StreamResult; 067import javax.xml.transform.stream.StreamSource; 068 069import org.apache.commons.cli.CommandLine; 070import org.apache.commons.cli.OptionBuilder; 071import org.apache.commons.cli.Options; 072import org.apache.commons.cli.ParseException; 073import org.dcm4che3.data.Attributes; 074import org.dcm4che3.data.ElementDictionary; 075import org.dcm4che3.data.Sequence; 076import org.dcm4che3.io.SAXReader; 077import org.dcm4che3.io.SAXWriter; 078import org.dcm4che3.json.JSONReader; 079import org.dcm4che3.json.JSONReader.Callback; 080import org.dcm4che3.tool.common.CLIUtils; 081import org.dcm4che3.util.SafeClose; 082import org.dcm4che3.util.TagUtils; 083import org.slf4j.Logger; 084import org.slf4j.LoggerFactory; 085import org.xml.sax.SAXException; 086 087/** 088 * QIDO-RS client 089 * 090 * @author Hesham Elbadawi <bsdreko@gmail.com> 091 * @author Hermann Czedik-Eysenberg <hermann-agfa@czedik.net> 092 */ 093public class QidoRS { 094 095 private static final Logger LOG = LoggerFactory.getLogger(QidoRS.class); 096 097 private static Options options; 098 099 private static ResourceBundle rb = ResourceBundle 100 .getBundle("org.dcm4che3.tool.qidors.messages"); 101 102 private SAXTransformerFactory saxTransformer; 103 104 private Templates xsltTemplates; 105 106 private final HashMap<String, String> query =new HashMap<String, String>(); 107 108 private String[] includeField; 109 110 private File outDir; 111 112 private File xsltFile; 113 114 private String outFileName; 115 116 private String requestTimeOut; 117 118 private boolean xmlIndent = false; 119 120 private boolean xmlIncludeKeyword = true; 121 122 private boolean isJSON; 123 124 private boolean fuzzy=false; 125 126 private boolean timezone=false; 127 128 private String limit; 129 130 private String offset="0"; 131 132 private String url; 133 134 private ParserType parserType; 135 136 private Attributes queryAttrs; 137 138 private Attributes returnAttrs; 139 140 private boolean returnAll; 141 142 private boolean runningModeTest; 143 144 private final List<Attributes> responseAttrs = new ArrayList<Attributes>(); 145 146 private long timeFirst; 147 148 private int numMatches = 0; 149 150 public QidoRS() {} 151 152 public QidoRS(boolean fuzzy, boolean timezone, boolean returnAll, 153 String limit, String offset, Attributes queryAttrs, Attributes returnAttrs, String mediaType, String url) { 154 this.returnAttrs = returnAttrs; 155 this.url = url; 156 this.queryAttrs = queryAttrs; 157 this.isJSON = mediaType.equalsIgnoreCase("JSON"); 158 this.fuzzy = fuzzy; 159 this.timezone = timezone; 160 this.returnAll = returnAll; 161 this.limit = limit; 162 this.offset = offset; 163 } 164 165 @SuppressWarnings("static-access") 166 private static CommandLine parseComandLine(String[] args) 167 throws ParseException { 168 options = new Options(); 169 170 options.addOption(OptionBuilder.hasArgs(2).withArgName("[seq.]attr=value") 171 .withValueSeparator().withDescription(rb.getString("match")) 172 .create("m")); 173 174 options.addOption("i", "includefield", true, rb.getString("includefield")); 175 176 options.addOption("J", "json", true, rb.getString("json")); 177 178 options.addOption(null, "fuzzy", false, rb.getString("fuzzy")); 179 180 options.addOption(null, "timezone", false, rb.getString("timezone")); 181 182 options.addOption(null, "limit", true, rb.getString("limit")); 183 184 options.addOption(null, "offset", true, rb.getString("offset")); 185 186 options.addOption(null, "request-timeout", true, rb.getString("request-timeout")); 187 188 addOutputOptions(options); 189 190 CLIUtils.addCommonOptions(options); 191 192 return CLIUtils.parseComandLine(args,options, rb, QidoRS.class); 193 } 194 195 @SuppressWarnings("static-access") 196 private static void addOutputOptions(Options opts) { 197 opts.addOption(OptionBuilder 198 .withLongOpt("out-dir") 199 .hasArg() 200 .withArgName("directory") 201 .withDescription(rb.getString("out-dir")) 202 .create()); 203 opts.addOption(OptionBuilder 204 .withLongOpt("out-file") 205 .hasArg() 206 .withArgName("name") 207 .withDescription(rb.getString("out-file")) 208 .create()); 209 opts.addOption("J", "json", false, rb.getString("json")); 210 opts.addOption(OptionBuilder 211 .withLongOpt("xsl") 212 .hasArg() 213 .withArgName("xsl-file") 214 .withDescription(rb.getString("xsl")) 215 .create("x")); 216 opts.addOption("I", "indent", false, rb.getString("indent")); 217 opts.addOption("K", "no-keyword", false, rb.getString("no-keyword")); 218 } 219 220 public static void main(String[] args) { 221 CommandLine cl = null; 222 try { 223 QidoRS main = new QidoRS(); 224 cl = parseComandLine(args); 225 main.setQuery(cl.getOptionProperties("m")); 226 if (cl.hasOption("out-dir")) 227 main.setOutDir(new File(cl.getOptionValue("out-dir"))); 228 229 if(cl.hasOption("out-file")) 230 main.setOutFileName(cl.getOptionValue("out-file", cl.getOptionValue("out-file"))); 231 else 232 main.setOutFileName(cl.getOptionValue("out-file", "qidoResponse")); 233 234 if (cl.hasOption("x")) 235 main.setXsltFile(new File(cl.getOptionValue("x"))); 236 237 if(cl.hasOption("J")) { 238 main.setJSON(true); 239 main.parserType = ParserType.JSON; 240 main.setOutFileName(main.getOutFileName()+".json"); 241 } 242 else{ 243 main.setJSON(false); 244 main.parserType = ParserType.XML; 245 main.setOutFileName(main.getOutFileName()+".xml"); 246 } 247 248 if(cl.hasOption("fuzzy")) 249 main.setFuzzy(true); 250 251 if(cl.hasOption("timezone")) 252 main.setTimezone(true); 253 254 if(cl.hasOption("limit")) 255 main.setLimit(cl.getOptionValue("limit")); 256 257 if(cl.hasOption("offset")) 258 main.setOffset(cl.getOptionValue("offset")); 259 260 main.setXmlIndent(cl.hasOption("I")); 261 262 main.setXmlIncludeKeyword(!cl.hasOption("K")); 263 264 main.setIncludeField(cl.getOptionValues("includefield")); 265 266 if(cl.hasOption("request-timeout")) 267 main.setRequestTimeOut(cl.getOptionValue("request-timout")); 268 269 main.setUrl(cl.getArgs()[0]); 270 271 String response = null; 272 try { 273 response = qido(main,true); 274 } catch (IOException e) { 275 System.out.print("Error during request {}"+e); 276 } 277 278 System.out.print(response); 279 } catch (Exception e) { 280 LOG.error("qidors: " + e.getMessage()); 281 System.err.println(rb.getString("try")); 282 System.exit(2); 283 } 284 } 285 286 public static String qido(QidoRS main, boolean cli) throws IOException { 287 URL newUrl; 288 if (cli) 289 newUrl = new URL(addRequestParametersCLI(main, main.getUrl())); 290 else 291 newUrl = new URL(addRequestParameters(main, main.getUrl())); 292 return sendRequest(newUrl, main); 293 } 294 295 private static String sendRequest(URL url, final QidoRS main) throws IOException { 296 297 LOG.info("URL: {}", url); 298 299 HttpURLConnection connection = (HttpURLConnection) url 300 .openConnection(); 301 302 connection.setDoOutput(true); 303 304 connection.setDoInput(true); 305 306 connection.setInstanceFollowRedirects(false); 307 308 connection.setRequestMethod("GET"); 309 310 connection.setRequestProperty("charset", "utf-8"); 311 312 if(main.isJSON) { 313 connection.setRequestProperty("Accept", "application/json"); 314 } 315 316 if(main.getRequestTimeOut()!=null) { 317 connection.setConnectTimeout(Integer.valueOf(main.getRequestTimeOut())); 318 connection.setReadTimeout(Integer.valueOf(main.getRequestTimeOut())); 319 } 320 321 connection.setUseCaches(false); 322 323 String response = "Server responded with " 324 +connection.getResponseCode() + " - " 325 +connection.getResponseMessage(); 326 327 InputStream in = connection.getInputStream(); 328 try { 329 main.parserType.readBody(main, in); 330 } catch (Exception e) { 331 System.out.println("Error parsing Server response - "+e); 332 } 333 connection.disconnect(); 334 335 return response; 336 337 } 338 339 private static String addRequestParametersCLI(final QidoRS main, String url) throws UnsupportedEncodingException { 340 341 if(main.includeField!=null) { 342 for(String field : main.getIncludeField()) 343 url=addParam(url,"includefield",field); 344 } 345 else { 346 url=addParam(url,"includefield","all"); 347 } 348 349 350 if(main.getQuery() != null) { 351 for(String queryKey : main.getQuery().keySet()) 352 url=addParam(url,queryKey,main.getQuery().get(queryKey)); 353 } 354 355 if(main.isFuzzy()) 356 url=addParam(url,"fuzzymatching","true"); 357 358 if(main.isTimezone()) 359 url=addParam(url,"timezoneadjustment","true"); 360 361 if(main.getLimit()!=null) { 362 url=addParam(url,"limit",main.getLimit()); 363 } 364 365 if(main.getOffset() != null) { 366 url=addParam(url,"offset",main.getOffset()); 367 } 368 return url; 369 } 370 371 private static String addRequestParameters(final QidoRS main, String url) throws UnsupportedEncodingException { 372 373 ElementDictionary dict = ElementDictionary.getStandardElementDictionary(); 374 375 if (!main.returnAll) { 376 if (main.returnAttrs != null) { 377 for (int tag : main.returnAttrs.tags()) { 378 if (!TagUtils.isPrivateCreator(tag)) { 379 url = addParam(url, "includefield", ElementDictionary.keywordOf(tag, main.returnAttrs.getPrivateCreator(tag))); 380 } 381 } 382 } 383 } else { 384 url = addParam(url, "includefield", "all"); 385 } 386 387 if(main.getQueryAttrs() != null) { 388 for(int i=0; i< main.queryAttrs.tags().length;i++) { 389 int tag = main.queryAttrs.tags()[i]; 390 String keyword ; 391 keyword = keyWordOf(main, dict, tag, main.queryAttrs); 392 if(main.queryAttrs.getSequence(tag) != null) { 393 //is a sequence 394 setSequenceQueryAttrs(main,url,main.queryAttrs.getSequence(tag), keyword); 395 } 396 else { 397 url=addParam(url, 398 keyword, (String) main.queryAttrs.getValue(tag)); 399 } 400 401 } 402 } 403 404 if(main.isFuzzy()) 405 url=addParam(url,"fuzzymatching","true"); 406 407 if(main.isTimezone()) 408 url=addParam(url,"timezoneadjustment","true"); 409 410 if(main.getLimit()!=null) { 411 url=addParam(url,"limit",main.getLimit()); 412 } 413 414 if(main.getOffset() != null) { 415 url=addParam(url,"offset",main.getOffset()); 416 } 417 if(main.isJSON) 418 main.parserType = ParserType.JSON; 419 else 420 main.parserType = ParserType.XML; 421 return url; 422 } 423 424 protected static String keyWordOf(final QidoRS main, 425 ElementDictionary dict, int tag, Attributes attrs) { 426 String keyword; 427 if(attrs.getPrivateCreator(tag) != null) { 428 keyword = ElementDictionary.keywordOf(tag, attrs.getPrivateCreator(tag)); 429 } 430 else { 431 keyword = dict.keywordOf(tag); 432 } 433 return keyword; 434 } 435 436 private static void setSequenceQueryAttrs(QidoRS main, String url, Sequence sequence, String seqKeyWork) { 437 ElementDictionary dict = ElementDictionary.getStandardElementDictionary(); 438 for(Attributes item : sequence) { 439 for(int i=0; i < item.tags().length; i++) { 440 int tag = item.tags()[i]; 441 if(item.getSequence(tag) == null) { 442 url+=(url.endsWith(".")?"":(url.contains("?")?"&":"?")) 443 +keyWordOf(main, dict, tag, main.queryAttrs) 444 +"="+(String) main.queryAttrs.getValue(tag); 445 } 446 else { 447 url+=(url.endsWith(".")?"":(url.contains("?")?"&":"?")) 448 +keyWordOf(main, dict, tag, main.queryAttrs)+"."; 449 setSequenceQueryAttrs(main,url,main.queryAttrs.getSequence(tag) 450 , keyWordOf(main, dict, tag, main.queryAttrs)); 451 } 452 } 453 454 } 455 } 456 457 private static String addParam(String url, String key, String field) throws UnsupportedEncodingException { 458 if (url.contains("?")) 459 return url += "&" + key + "=" + URLEncoder.encode(field, "UTF-8"); 460 else 461 return url += "?" + key + "=" + URLEncoder.encode(field, "UTF-8"); 462 } 463 464 private enum ParserType { 465 XML { 466 @Override 467 boolean readBody(QidoRS qidors, InputStream in) throws Exception { 468 469 String full=""; 470 String str; 471 BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8")); 472 String boundary = reader.readLine(); 473 while((str = reader.readLine())!=null) { 474 full+=str; 475 } 476 477 String[] parts = full.split(boundary); 478 479 for(int i=0;i<parts.length-1;i++) { 480 if(qidors.isRunningModeTest()) { 481 if(qidors.getTimeFirst() == 0) 482 qidors.setTimeFirst(System.currentTimeMillis()); 483 qidors.responseAttrs.add(SAXReader.parse(new ByteArrayInputStream(removeHeader(parts[i]).getBytes("UTF-8")))); 484 qidors.numMatches++; 485 } 486 else { 487 File out = new File(qidors.outDir, "part - "+(i+1)+" - "+qidors.outFileName); 488 TransformerHandler th = getTransformerHandler(qidors); 489 th.getTransformer().setOutputProperty(OutputKeys.INDENT, 490 qidors.xmlIndent ? "yes" : "no"); 491 th.setResult(new StreamResult(out)); 492 Attributes attrs= SAXReader.parse(new ByteArrayInputStream(removeHeader(parts[i]).getBytes())); 493 SAXWriter saxWriter = new SAXWriter(th); 494 saxWriter.setIncludeKeyword(qidors.xmlIncludeKeyword); 495 saxWriter.write(attrs); 496 } 497 } 498 499 reader.close(); 500 return true; 501 502 } 503 private TransformerHandler getTransformerHandler(QidoRS main) throws Exception { 504 505 SAXTransformerFactory stf = main.saxTransformer; 506 507 if (stf == null) 508 main.saxTransformer = stf = (SAXTransformerFactory) TransformerFactory 509 .newInstance(); 510 511 if (main.getXsltFile() == null) 512 return stf.newTransformerHandler(); 513 514 Templates templates = main.xsltTemplates; 515 516 if (templates == null){ 517 templates = stf.newTemplates(new StreamSource(main.xsltFile)); 518 main.xsltTemplates = templates; 519 } 520 521 return stf.newTransformerHandler(templates); 522 } 523 524 private String removeHeader(String str) { 525 String buff=""; 526 527 for(int i=0;i<str.length();i++) 528 if(str.charAt(i) == '<') { 529 if(str.charAt(i+1)=='?') { 530 buff+=str.substring(i,str.length()); 531 break; 532 } 533 } 534 return buff; 535 } 536 }, 537 JSON { 538 @Override 539 boolean readBody(final QidoRS qidors, InputStream in) 540 throws IOException, ParserConfigurationException, SAXException { 541 if(qidors.isRunningModeTest()) { 542 try { 543 JSONReader reader = new JSONReader( 544 Json.createParser(new InputStreamReader(in, "UTF-8"))); 545 reader.readDatasets(new Callback() { 546 @Override 547 public void onDataset(Attributes fmi, Attributes dataset) { 548 if(qidors.getTimeFirst() == 0) 549 qidors.setTimeFirst(System.currentTimeMillis()); 550 qidors.responseAttrs.add(dataset); 551 qidors.numMatches++; 552 } 553 }); 554 555 } finally { 556 if (in != System.in) 557 SafeClose.close(in); 558 } 559 560 } 561 else { 562 Files.copy(in, new File(qidors.outDir, qidors.outFileName).toPath() 563 , StandardCopyOption.REPLACE_EXISTING); 564 } 565 return true; 566 } 567 }; 568 569 abstract boolean readBody(QidoRS qidors, InputStream in) 570 throws IOException, Exception; 571 572 } 573 574 public String[] getIncludeField() { 575 return includeField; 576 } 577 578 public void setIncludeField(String[] includeField) { 579 this.includeField = includeField; 580 } 581 582 public File getOutDir() { 583 return outDir; 584 } 585 586 public void setOutDir(File outDir) { 587 outDir.mkdirs(); 588 this.outDir = outDir; 589 } 590 591 public File getXsltFile() { 592 return xsltFile; 593 } 594 595 public void setXsltFile(File xsltFile) { 596 this.xsltFile = xsltFile; 597 } 598 599 public String getOutFileName() { 600 return outFileName; 601 } 602 603 public void setOutFileName(String outFileName) { 604 this.outFileName = outFileName; 605 } 606 607 public String getRequestTimeOut() { 608 return requestTimeOut; 609 } 610 611 public void setRequestTimeOut(String requestTimeOut) { 612 this.requestTimeOut = requestTimeOut; 613 } 614 615 public boolean isXmlIndent() { 616 return xmlIndent; 617 } 618 619 public void setXmlIndent(boolean xmlIndent) { 620 this.xmlIndent = xmlIndent; 621 } 622 623 public boolean isXmlIncludeKeyword() { 624 return xmlIncludeKeyword; 625 } 626 627 public void setXmlIncludeKeyword(boolean xmlIncludeKeyword) { 628 this.xmlIncludeKeyword = xmlIncludeKeyword; 629 } 630 631 public boolean isJSON() { 632 return isJSON; 633 } 634 635 public boolean isRunningModeTest() { 636 return runningModeTest; 637 } 638 639 public void setRunningModeTest(boolean runningModeTest) { 640 this.runningModeTest = runningModeTest; 641 } 642 643 public void setJSON(boolean isJSON) { 644 this.isJSON = isJSON; 645 } 646 647 public boolean isFuzzy() { 648 return fuzzy; 649 } 650 651 public void setFuzzy(boolean fuzzy) { 652 this.fuzzy = fuzzy; 653 } 654 655 public boolean isTimezone() { 656 return timezone; 657 } 658 659 public void setTimezone(boolean timezone) { 660 this.timezone = timezone; 661 } 662 663 public HashMap<String, String> getQuery() { 664 return query; 665 } 666 667 private void setQuery(Properties optionProperties) { 668 for(Object key : optionProperties.keySet()) 669 this.query.put((String) key, (String) optionProperties.get(key)); 670 } 671 672 public String getUrl() { 673 return url; 674 } 675 676 public void setUrl(String url) { 677 this.url = url; 678 } 679 680 public String getLimit() { 681 return limit; 682 } 683 684 public void setLimit(String limit) { 685 this.limit = limit; 686 } 687 688 public String getOffset() { 689 return offset; 690 } 691 692 public void setOffset(String offset) { 693 this.offset = offset; 694 } 695 696 public Attributes getQueryAttrs() { 697 return queryAttrs; 698 } 699 700 public void setQueryAttrs(Attributes queryAttrs) { 701 this.queryAttrs = queryAttrs; 702 } 703 704 public long getTimeFirst() { 705 return timeFirst; 706 } 707 708 public void setTimeFirst(long timeFirst) { 709 this.timeFirst = timeFirst; 710 } 711 712 public int getNumMatches() { 713 return numMatches; 714 } 715 716 public List<Attributes> getResponseAttrs() { 717 return responseAttrs; 718 } 719 720 public Attributes getReturnAttrs() { 721 return returnAttrs; 722 } 723 724 725}