org.dcm4che3.tool.stowrs.StowRS.java Source code

Java tutorial

Introduction

Here is the source code for org.dcm4che3.tool.stowrs.StowRS.java

Source

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in
 * Java(TM), hosted at https://github.com/gunterze/dcm4che.
 *
 * The Initial Developer of the Original Code is
 * Agfa Healthcare.
 * Portions created by the Initial Developer are Copyright (C) 2012
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 * See @authors listed below
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.dcm4che3.tool.stowrs;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;
import java.util.UUID;

import javax.json.Json;
import javax.json.stream.JsonGenerator;
import javax.ws.rs.core.MediaType;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.MissingArgumentException;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Attributes.Visitor;
import org.dcm4che3.data.BulkData;
import org.dcm4che3.data.Fragments;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.VR;
import org.dcm4che3.io.ContentHandlerAdapter;
import org.dcm4che3.io.SAXReader;
import org.dcm4che3.io.SAXTransformer;
import org.dcm4che3.json.JSONReader;
import org.dcm4che3.json.JSONWriter;
import org.dcm4che3.tool.common.CLIUtils;
import org.dcm4che3.tool.stowrs.test.StowRSResponse;
import org.dcm4che3.tool.stowrs.test.StowRSTool.StowMetaDataType;
import org.dcm4che3.util.SafeClose;
import org.dcm4che3.util.StreamUtils;
import org.dcm4che3.ws.rs.MediaTypes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

/**
 * STOW-RS client.
 * 
 * @author Hesham Elbadawi <bsdreko@gmail.com>
 * @author Hermann Czedik-Eysenberg <hermann-agfa@czedik.net>
 */
public class StowRS {

    private static final Logger LOG = LoggerFactory.getLogger(StowRS.class);

    private static final String MULTIPART_BOUNDARY = "-------gc0p4Jq0M2Yt08jU534c0p";

    private Attributes keys = new Attributes();
    private static Options opts;
    private String URL;
    private final List<StowRSResponse> responses = new ArrayList<StowRSResponse>();
    private static ResourceBundle rb = ResourceBundle.getBundle("org.dcm4che3.tool.stowrs.messages");

    private StowMetaDataType mediaType;
    private String transferSyntax;
    private List<File> files = new ArrayList<File>();

    public StowRS() {
        // empty
    }

    public StowRS(Attributes overrideAttrs, StowMetaDataType mediaType, List<File> files, String url, String ts) {
        this.URL = url;
        this.keys = overrideAttrs;
        this.transferSyntax = ts;
        this.mediaType = mediaType;
        this.files = files;
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        CommandLine cl = null;
        try {

            cl = parseComandLine(args);
            StowRS instance = new StowRS();
            if (cl.hasOption("m"))
                instance.keys = configureKeys(instance, cl);
            if (!cl.hasOption("u")) {
                throw new IllegalArgumentException("Missing url");
            } else {
                instance.URL = cl.getOptionValue("u");
            }

            if (cl.hasOption("t")) {
                if (!cl.hasOption("ts")) {
                    throw new MissingArgumentException("Missing option required option ts when sending metadata");
                } else {
                    instance.setTransferSyntax(cl.getOptionValue("ts"));
                }

                String mediaTypeString = cl.getOptionValue("t");
                if ("JSON".equalsIgnoreCase(mediaTypeString)) {
                    instance.mediaType = StowMetaDataType.JSON;
                } else if ("XML".equalsIgnoreCase(mediaTypeString)) {
                    instance.mediaType = StowMetaDataType.XML;
                } else {
                    throw new IllegalArgumentException(
                            "Bad Type " + mediaTypeString + " specified for metadata, specify either XML or JSON");
                }
            } else {
                instance.mediaType = StowMetaDataType.NO_METADATA_DICOM;
            }

            for (Iterator<String> iter = cl.getArgList().iterator(); iter.hasNext();) {
                instance.files.add(new File(iter.next()));
            }

            if (instance.files.isEmpty())
                throw new IllegalArgumentException("Missing files");

            instance.stow();

        } catch (Exception e) {
            if (!cl.hasOption("u")) {
                LOG.error("stowrs: missing required option -u");
                LOG.error("Try 'stowrs --help' for more information.");
                System.exit(2);
            } else {
                LOG.error("Error: \n", e);
                e.printStackTrace();
            }

        }
    }

    public void stow() {

        for (File file : files) {

            LOG.info("Sending {}", file);

            if (mediaType == StowMetaDataType.NO_METADATA_DICOM) {
                stowDicomFile(file);
            } else {
                stowMetaDataAndBulkData(file);
            }
        }
    }

    private void stowMetaDataAndBulkData(File file) {

        Attributes metadata;
        if (mediaType == StowMetaDataType.JSON) {
            try {
                metadata = parseJSON(file.getPath());
            } catch (Exception e) {
                LOG.error("error parsing metadata JSON file {}", file);
                return;
            }
        } else if (mediaType == StowMetaDataType.XML) {

            metadata = new Attributes();
            try {
                ContentHandlerAdapter ch = new ContentHandlerAdapter(metadata);
                SAXParserFactory.newInstance().newSAXParser().parse(file, ch);
                Attributes fmi = ch.getFileMetaInformation();
                if (fmi != null)
                    metadata.addAll(fmi);
            } catch (Exception e) {
                LOG.error("error parsing metadata XML file {}", file);
                return;
            }
        } else {
            throw new IllegalArgumentException("Unsupported media type " + mediaType);
        }

        ExtractedBulkData extractedBulkData = extractBulkData(metadata);

        if (isMultiFrame(metadata)) {

            if (extractedBulkData.pixelDataBulkData.size() > 1) {

                // multiple fragments - reject
                LOG.error(
                        "Compressed multiframe with multiple fragments in file {} is not supported by STOW-RS in the current DICOM standard (2015b)",
                        file);

                return;
            }
        }

        if (!extractedBulkData.pixelDataBulkData.isEmpty()) {

            // replace the pixel data bulk data URI, because we might have to merge multiple fragments into one

            metadata.setValue(Tag.PixelData, metadata.getVR(Tag.PixelData), new BulkData(null,
                    extractedBulkData.pixelDataBulkDataURI, extractedBulkData.pixelDataBulkData.get(0).bigEndian));
        }

        try {
            addResponse(sendMetaDataAndBulkData(metadata, extractedBulkData));
        } catch (IOException e) {
            LOG.error("Error for file {}", file, e);
        }
    }

    private void stowDicomFile(File file) {
        try {

            addResponse(sendDicomFile(URL, file));
            LOG.info(file.getPath() + " with size : " + file.length());

        } catch (IOException e) {
            LOG.error("Error for file {}", file, e);
        }
    }

    private static class ExtractedBulkData {
        final List<BulkData> pixelDataBulkData = new ArrayList<BulkData>();
        final List<BulkData> otherBulkDataChunks = new ArrayList<BulkData>();

        final String pixelDataBulkDataURI = createRandomBulkDataURI();
    }

    private static String createRandomBulkDataURI() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    private ExtractedBulkData extractBulkData(Attributes dataset) {

        final ExtractedBulkData extractedBulkData = new ExtractedBulkData();

        try {
            dataset.accept(new Visitor() {

                @Override
                public boolean visit(Attributes attrs, int tag, VR vr, Object value) {

                    if (attrs.isRoot() && tag == Tag.PixelData) {
                        if (value instanceof BulkData) {
                            extractedBulkData.pixelDataBulkData.add((BulkData) value);
                        } else if (value instanceof Fragments) {
                            Fragments frags = (Fragments) value;
                            if (frags.size() > 1 && frags.get(1) instanceof BulkData) {
                                // please note that we are ignoring the first fragment (offset table) here
                                // (it's okay as we are anyways not supporting fragmented multi-frames at the moment)
                                for (int i = 1; i < frags.size(); i++) {
                                    if (frags.get(i) instanceof BulkData) {
                                        extractedBulkData.pixelDataBulkData.add((BulkData) frags.get(i));
                                    }
                                }
                            }
                        }
                    } else {
                        // other bulk data tags (not top-level pixel data)

                        if (value instanceof BulkData) {
                            extractedBulkData.otherBulkDataChunks.add((BulkData) value);
                        }

                        // Note: at the moment we support fragments only for top-level PixelData.
                        // Maybe we should also support it for others, seems to be at least allowed for PixelData inside sequences
                        // (see DICOM PS3.5 2015b A.4 Transfer Syntaxes For Encapsulation of Encoded Pixel Data)
                    }

                    return true;
                }
            }, true);
        } catch (Exception e) {
            throw new RuntimeException(e); // should not happen
        }

        return extractedBulkData;
    }

    public List<StowRSResponse> getResponses() {
        return responses;
    }

    public void addResponse(StowRSResponse response) {
        this.responses.add(response);
    }

    private static Attributes parseJSON(String fname) throws Exception {
        Attributes attrs = new Attributes();
        Attributes fmi = parseJSON(fname, attrs);
        if (fmi != null)
            attrs.addAll(fmi);
        return attrs;
    }

    @SuppressWarnings("static-access")
    private static CommandLine parseComandLine(String[] args) throws ParseException {
        opts = new Options();
        opts.addOption(OptionBuilder.hasArgs(2).withArgName("[seq/]attr=value").withValueSeparator()
                .withDescription(rb.getString("metadata")).create("m"));
        opts.addOption("u", "url", true, rb.getString("url"));
        opts.addOption("t", "metadata-type", true, rb.getString("metadata-type"));
        opts.addOption("ts", "transfer-syntax", true, rb.getString("transfer-syntax"));
        CLIUtils.addCommonOptions(opts);
        return CLIUtils.parseComandLine(args, opts, rb, StowRS.class);
    }

    private static Attributes configureKeys(StowRS main, CommandLine cl) {
        Attributes temp = new Attributes();
        CLIUtils.addAttributes(temp, cl.getOptionValues("m"));
        LOG.info("added keys for coercion: \n" + main.keys.toString());
        return temp;
    }

    private static boolean isMultiFrame(Attributes metadata) {
        return metadata.contains(Tag.NumberOfFrames) && metadata.getInt(Tag.NumberOfFrames, 1) > 1;
    }

    private static void coerceAttributes(Attributes metadata, Attributes keys) {
        if (!keys.isEmpty()) {
            LOG.info("Coercing the following keys from specified attributes to metadata:");
            metadata.update(keys, null);
            LOG.info(keys.toString());
        }
    }

    private StowRSResponse sendMetaDataAndBulkData(Attributes metadata, ExtractedBulkData extractedBulkData)
            throws IOException {
        Attributes responseAttrs = new Attributes();

        URL newUrl;
        try {
            newUrl = new URL(URL);
        } catch (MalformedURLException e2) {
            throw new RuntimeException(e2);
        }

        HttpURLConnection connection = (HttpURLConnection) newUrl.openConnection();
        connection.setChunkedStreamingMode(2048);
        connection.setDoOutput(true);
        connection.setDoInput(true);
        connection.setInstanceFollowRedirects(false);
        connection.setRequestMethod("POST");

        String metaDataType = mediaType == StowMetaDataType.XML ? "application/dicom+xml" : "application/json";
        connection.setRequestProperty("Content-Type",
                "multipart/related; type=" + metaDataType + "; boundary=" + MULTIPART_BOUNDARY);
        String bulkDataTransferSyntax = "transfer-syntax=" + transferSyntax;

        MediaType pixelDataMediaType = getBulkDataMediaType(metadata);
        connection.setRequestProperty("Accept", "application/dicom+xml");
        connection.setRequestProperty("charset", "utf-8");
        connection.setUseCaches(false);

        DataOutputStream wr = new DataOutputStream(connection.getOutputStream());

        // write metadata
        wr.writeBytes("\r\n--" + MULTIPART_BOUNDARY + "\r\n");

        if (mediaType == StowMetaDataType.XML)
            wr.writeBytes("Content-Type: application/dicom+xml; " + bulkDataTransferSyntax + " \r\n");
        else
            wr.writeBytes("Content-Type: application/json; " + bulkDataTransferSyntax + " \r\n");
        wr.writeBytes("\r\n");

        coerceAttributes(metadata, keys);

        try {
            if (mediaType == StowMetaDataType.XML)
                SAXTransformer.getSAXWriter(new StreamResult(wr)).write(metadata);
            else {
                JsonGenerator gen = Json.createGenerator(wr);
                JSONWriter writer = new JSONWriter(gen);
                writer.write(metadata);
                gen.flush();
            }
        } catch (TransformerConfigurationException e) {
            throw new IOException(e);
        } catch (SAXException e) {
            throw new IOException(e);
        }

        // write bulkdata

        for (BulkData chunk : extractedBulkData.otherBulkDataChunks) {
            writeBulkDataPart(MediaType.APPLICATION_OCTET_STREAM_TYPE, wr, chunk.getURIOrUUID(),
                    Collections.singletonList(chunk));
        }

        if (!extractedBulkData.pixelDataBulkData.isEmpty()) {
            // pixeldata as a single bulk data part

            if (extractedBulkData.pixelDataBulkData.size() > 1) {
                LOG.info("Combining bulk data of multiple pixel data fragments");
            }

            writeBulkDataPart(pixelDataMediaType, wr, extractedBulkData.pixelDataBulkDataURI,
                    extractedBulkData.pixelDataBulkData);
        }

        // end of multipart message
        wr.writeBytes("\r\n--" + MULTIPART_BOUNDARY + "--\r\n");
        wr.close();
        String response = connection.getResponseMessage();
        int rspCode = connection.getResponseCode();
        LOG.info("response: " + response);
        try {
            responseAttrs = SAXReader.parse(connection.getInputStream());
        } catch (Exception e) {
            LOG.error("Error creating response attributes", e);
        }
        connection.disconnect();

        return new StowRSResponse(rspCode, response, responseAttrs);
    }

    private static void writeBulkDataPart(MediaType mediaType, DataOutputStream wr, String uri,
            List<BulkData> chunks) throws IOException {
        wr.writeBytes("\r\n--" + MULTIPART_BOUNDARY + "\r\n");
        wr.writeBytes("Content-Type: " + toContentType(mediaType) + " \r\n");
        wr.writeBytes("Content-Location: " + uri + " \r\n");
        wr.writeBytes("\r\n");

        for (BulkData chunk : chunks) {
            writeBulkDataToStream(chunk, wr);
        }
    }

    private static String toContentType(MediaType mediaType) {
        StringBuilder sb = new StringBuilder();
        sb.append(mediaType.getType()).append('/').append(mediaType.getSubtype());
        String tsuid = mediaType.getParameters().get("transfer-syntax");
        if (tsuid != null) {
            sb.append("; transfer-syntax=").append(tsuid);
        }
        return sb.toString();
    }

    private MediaType getBulkDataMediaType(Attributes metadata) {
        return MediaTypes.forTransferSyntax(metadata.getString(Tag.TransferSyntaxUID, getTransferSyntax()));
    }

    private static void writeBulkDataToStream(BulkData bulkData, DataOutputStream wr) throws IOException {
        InputStream in = null;
        try {
            in = bulkData.openStream();

            int length = bulkData.length();

            if (length >= 0) {
                StreamUtils.copy(in, wr, length);
            } else { // unspecified length
                StreamUtils.copy(in, wr);
            }

        } finally {
            if (in != null)
                try {
                    in.close();
                } catch (IOException e) {
                    LOG.error("Error closing stream", e);
                }
        }
    }

    private static StowRSResponse sendDicomFile(String url, File f) throws IOException {
        int rspCode = 0;
        String rspMessage = null;

        URL newUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) newUrl.openConnection();
        connection.setDoOutput(true);
        connection.setDoInput(true);
        connection.setInstanceFollowRedirects(false);
        connection.setRequestMethod("POST");
        connection.setRequestProperty("Content-Type",
                "multipart/related; type=application/dicom; boundary=" + MULTIPART_BOUNDARY);
        connection.setRequestProperty("Accept", "application/dicom+xml");
        connection.setRequestProperty("charset", "utf-8");
        connection.setUseCaches(false);

        DataOutputStream wr;
        wr = new DataOutputStream(connection.getOutputStream());
        wr.writeBytes("\r\n--" + MULTIPART_BOUNDARY + "\r\n");
        wr.writeBytes("Content-Disposition: inline; name=\"file[]\"; filename=\"" + f.getName() + "\"\r\n");
        wr.writeBytes("Content-Type: application/dicom \r\n");
        wr.writeBytes("\r\n");
        FileInputStream fis = new FileInputStream(f);
        StreamUtils.copy(fis, wr);
        fis.close();
        wr.writeBytes("\r\n--" + MULTIPART_BOUNDARY + "--\r\n");
        wr.flush();
        wr.close();
        String response = connection.getResponseMessage();
        rspCode = connection.getResponseCode();
        rspMessage = connection.getResponseMessage();
        LOG.info("response: " + response);
        Attributes responseAttrs = null;
        try {
            InputStream in;
            boolean isErrorCase = rspCode >= HttpURLConnection.HTTP_BAD_REQUEST;
            if (!isErrorCase) {
                in = connection.getInputStream();
            } else {
                in = connection.getErrorStream();
            }
            if (!isErrorCase || rspCode == HttpURLConnection.HTTP_CONFLICT)
                responseAttrs = SAXReader.parse(in);
        } catch (SAXException e) {
            throw new IOException(e);
        } catch (ParserConfigurationException e) {
            throw new IOException(e);
        }
        connection.disconnect();

        return new StowRSResponse(rspCode, rspMessage, responseAttrs);
    }

    private static Attributes parseJSON(String fname, Attributes attrs) throws IOException {
        InputStream in = fname.equals("-") ? System.in : new FileInputStream(fname);
        try {
            JSONReader reader = new JSONReader(Json.createParser(new InputStreamReader(in, "UTF-8")));
            reader.readDataset(attrs);
            Attributes fmi = reader.getFileMetaInformation();
            return fmi;
        } finally {
            if (in != System.in)
                SafeClose.close(in);
        }
    }

    public String getTransferSyntax() {
        return transferSyntax;
    }

    public void setTransferSyntax(String transferSyntax) {
        this.transferSyntax = transferSyntax;
    }

}