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}