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}