Java tutorial
/* ***** 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) 2011 * 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 edu.wustl.mir.erl.ihe.xdsi.util; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.security.GeneralSecurityException; import java.text.MessageFormat; import java.util.List; import java.util.ListResourceBundle; import java.util.Properties; import java.util.ResourceBundle; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.cli.CommandLine; 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.Tag; import org.dcm4che3.data.UID; import org.dcm4che3.imageio.codec.Decompressor; import org.dcm4che3.io.DicomInputStream; import org.dcm4che3.io.DicomInputStream.IncludeBulkData; import org.dcm4che3.io.SAXReader; import org.dcm4che3.net.ApplicationEntity; import org.dcm4che3.net.Association; import org.dcm4che3.net.Connection; import org.dcm4che3.net.DataWriterAdapter; import org.dcm4che3.net.Device; import org.dcm4che3.net.DimseRSP; import org.dcm4che3.net.DimseRSPHandler; import org.dcm4che3.net.IncompatibleConnectionException; import org.dcm4che3.net.InputStreamDataWriter; import org.dcm4che3.net.Status; import org.dcm4che3.net.pdu.AAssociateRQ; import org.dcm4che3.net.pdu.PresentationContext; import org.dcm4che3.tool.common.CLIUtils; import org.dcm4che3.tool.common.DicomFiles; import org.dcm4che3.util.SafeClose; import org.dcm4che3.util.StringUtils; import org.dcm4che3.util.TagUtils; import org.xml.sax.SAXException; /** * @author Gunter Zeilinger gunterze@gmail.com * @author Michael Backhaus michael.backhaus@agfa.com */ @SuppressWarnings("javadoc") public class StoreSCU { public interface RSPHandlerFactory { DimseRSPHandler createDimseRSPHandler(File f); } private static ResourceBundle rb = new storeSCPResources(); // ResourceBundle.getBundle("org.dcm4che3.tool.storescu.messages"); private final ApplicationEntity ae; private final Connection remote; private final AAssociateRQ rq = new AAssociateRQ(); private final RelatedGeneralSOPClasses relSOPClasses = new RelatedGeneralSOPClasses(); private Attributes attrs; private String uidSuffix; private boolean relExtNeg; private int priority; private String tmpPrefix = "storescu-"; private String tmpSuffix; private File tmpDir; private File tmpFile; private Association as; private long totalSize; private int filesScanned; private int filesSent; private RSPHandlerFactory rspHandlerFactory = new RSPHandlerFactory() { @Override public DimseRSPHandler createDimseRSPHandler(final File f) { return new DimseRSPHandler(as.nextMessageID()) { @Override public void onDimseRSP(@SuppressWarnings("hiding") Association as, Attributes cmd, Attributes data) { super.onDimseRSP(as, cmd, data); StoreSCU.this.onCStoreRSP(cmd, f); } }; } }; @SuppressWarnings("unused") public StoreSCU(ApplicationEntity ae) throws IOException { this.remote = new Connection(); this.ae = ae; rq.addPresentationContext(new PresentationContext(1, UID.VerificationSOPClass, UID.ImplicitVRLittleEndian)); } public void setRspHandlerFactory(RSPHandlerFactory rspHandlerFactory) { this.rspHandlerFactory = rspHandlerFactory; } public AAssociateRQ getAAssociateRQ() { return rq; } public Connection getRemoteConnection() { return remote; } public Attributes getAttributes() { return attrs; } public void setAttributes(Attributes attrs) { this.attrs = attrs; } public void setTmpFile(File tmpFile) { this.tmpFile = tmpFile; } public final void setPriority(int priority) { this.priority = priority; } public final void setUIDSuffix(String uidSuffix) { this.uidSuffix = uidSuffix; } public final void setTmpFilePrefix(String prefix) { this.tmpPrefix = prefix; } public final void setTmpFileSuffix(String suffix) { this.tmpSuffix = suffix; } public final void setTmpFileDirectory(File tmpDir) { this.tmpDir = tmpDir; } private static CommandLine parseComandLine(String[] args) throws ParseException { Options opts = new Options(); CLIUtils.addConnectOption(opts); CLIUtils.addBindOption(opts, "STORESCU"); CLIUtils.addAEOptions(opts); CLIUtils.addResponseTimeoutOption(opts); CLIUtils.addPriorityOption(opts); CLIUtils.addCommonOptions(opts); addTmpFileOptions(opts); addRelatedSOPClassOptions(opts); addAttributesOption(opts); addUIDSuffixOption(opts); return CLIUtils.parseComandLine(args, opts, rb, StoreSCU.class); } @SuppressWarnings("static-access") private static void addAttributesOption(Options opts) { opts.addOption(OptionBuilder.hasArgs().withArgName("[seq/]attr=value").withValueSeparator('=') .withDescription(rb.getString("set")).create("s")); } @SuppressWarnings("static-access") public static void addUIDSuffixOption(Options opts) { opts.addOption(OptionBuilder.hasArg().withArgName("suffix").withDescription(rb.getString("uid-suffix")) .withLongOpt("uid-suffix").create(null)); } @SuppressWarnings("static-access") public static void addTmpFileOptions(Options opts) { opts.addOption(OptionBuilder.hasArg().withArgName("directory").withDescription(rb.getString("tmp-file-dir")) .withLongOpt("tmp-file-dir").create(null)); opts.addOption(OptionBuilder.hasArg().withArgName("prefix").withDescription(rb.getString("tmp-file-prefix")) .withLongOpt("tmp-file-prefix").create(null)); opts.addOption(OptionBuilder.hasArg().withArgName("suffix").withDescription(rb.getString("tmp-file-suffix")) .withLongOpt("tmp-file-suffix").create(null)); } @SuppressWarnings("static-access") private static void addRelatedSOPClassOptions(Options opts) { opts.addOption(null, "rel-ext-neg", false, rb.getString("rel-ext-neg")); opts.addOption(OptionBuilder.hasArg().withArgName("file|url") .withDescription(rb.getString("rel-sop-classes")).withLongOpt("rel-sop-classes").create(null)); } @SuppressWarnings("unchecked") public static void main(String[] args) { long t1, t2; try { CommandLine cl = parseComandLine(args); Device device = new Device("storescu"); Connection conn = new Connection(); device.addConnection(conn); ApplicationEntity ae = new ApplicationEntity("STORESCU"); device.addApplicationEntity(ae); ae.addConnection(conn); StoreSCU main = new StoreSCU(ae); configureTmpFile(main, cl); CLIUtils.configureConnect(main.remote, main.rq, cl); CLIUtils.configureBind(conn, ae, cl); CLIUtils.configure(conn, cl); main.remote.setTlsProtocols(conn.getTlsProtocols()); main.remote.setTlsCipherSuites(conn.getTlsCipherSuites()); configureRelatedSOPClass(main, cl); main.setAttributes(new Attributes()); CLIUtils.addAttributes(main.attrs, cl.getOptionValues("s")); main.setUIDSuffix(cl.getOptionValue("uid-suffix")); main.setPriority(CLIUtils.priorityOf(cl)); List<String> argList = cl.getArgList(); boolean echo = argList.isEmpty(); if (!echo) { System.out.println(rb.getString("scanning")); t1 = System.currentTimeMillis(); main.scanFiles(argList); t2 = System.currentTimeMillis(); int n = main.filesScanned; System.out.println(); if (n == 0) return; System.out.println( MessageFormat.format(rb.getString("scanned"), n, (t2 - t1) / 1000F, (t2 - t1) / n)); } ExecutorService executorService = Executors.newSingleThreadExecutor(); ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); device.setExecutor(executorService); device.setScheduledExecutor(scheduledExecutorService); try { t1 = System.currentTimeMillis(); main.open(); t2 = System.currentTimeMillis(); System.out .println(MessageFormat.format(rb.getString("connected"), main.as.getRemoteAET(), t2 - t1)); if (echo) main.echo(); else { t1 = System.currentTimeMillis(); main.sendFiles(); t2 = System.currentTimeMillis(); } } finally { main.close(); executorService.shutdown(); scheduledExecutorService.shutdown(); } if (main.filesScanned > 0) { float s = (t2 - t1) / 1000F; float mb = main.totalSize / 1048576F; System.out.println(MessageFormat.format(rb.getString("sent"), main.filesSent, mb, s, mb / s)); } } catch (ParseException e) { System.err.println("storescu: " + e.getMessage()); System.err.println(rb.getString("try")); System.exit(2); } catch (Exception e) { System.err.println("storescu: " + e.getMessage()); e.printStackTrace(); System.exit(2); } } public static String uidSuffixOf(CommandLine cl) { return cl.getOptionValue("uid-suffix"); } private static void configureTmpFile(StoreSCU storescu, CommandLine cl) { if (cl.hasOption("tmp-file-dir")) storescu.setTmpFileDirectory(new File(cl.getOptionValue("tmp-file-dir"))); storescu.setTmpFilePrefix(cl.getOptionValue("tmp-file-prefix", "storescu-")); storescu.setTmpFileSuffix(cl.getOptionValue("tmp-file-suffix")); } public static void configureRelatedSOPClass(StoreSCU storescu, CommandLine cl) throws IOException { if (cl.hasOption("rel-ext-neg")) { storescu.enableSOPClassRelationshipExtNeg(true); Properties p = new Properties(); CLIUtils.loadProperties(cl.getOptionValue("rel-sop-classes", "resource:rel-sop-classes.properties"), p); storescu.relSOPClasses.init(p); } } public final void enableSOPClassRelationshipExtNeg(boolean enable) { relExtNeg = enable; } public void scanFiles(List<String> fnames) throws IOException { this.scanFiles(fnames, true); } public void scanFiles(List<String> fnames, boolean printout) throws IOException { tmpFile = File.createTempFile(tmpPrefix, tmpSuffix, tmpDir); tmpFile.deleteOnExit(); final BufferedWriter fileInfos = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tmpFile))); try { DicomFiles.scan(fnames, printout, new DicomFiles.Callback() { @Override public boolean dicomFile(File f, Attributes fmi, long dsPos, Attributes ds) throws IOException { if (!addFile(fileInfos, f, dsPos, fmi, ds)) return false; filesScanned++; return true; } }); } finally { fileInfos.close(); } } public void sendFiles() throws IOException { BufferedReader fileInfos = new BufferedReader(new InputStreamReader(new FileInputStream(tmpFile))); try { String line; while (as.isReadyForDataTransfer() && (line = fileInfos.readLine()) != null) { String[] ss = StringUtils.split(line, '\t'); try { send(new File(ss[4]), Long.parseLong(ss[3]), ss[1], ss[0], ss[2]); } catch (Exception e) { e.printStackTrace(); } } try { as.waitForOutstandingRSP(); } catch (InterruptedException e) { e.printStackTrace(); } } finally { SafeClose.close(fileInfos); } } public boolean addFile(BufferedWriter fileInfos, File f, long endFmi, Attributes fmi, Attributes ds) throws IOException { String cuid = fmi.getString(Tag.MediaStorageSOPClassUID); String iuid = fmi.getString(Tag.MediaStorageSOPInstanceUID); String ts = fmi.getString(Tag.TransferSyntaxUID); if (cuid == null || iuid == null) return false; fileInfos.write(iuid); fileInfos.write('\t'); fileInfos.write(cuid); fileInfos.write('\t'); fileInfos.write(ts); fileInfos.write('\t'); fileInfos.write(Long.toString(endFmi)); fileInfos.write('\t'); fileInfos.write(f.getPath()); fileInfos.newLine(); if (rq.containsPresentationContextFor(cuid, ts)) return true; if (!rq.containsPresentationContextFor(cuid)) { if (relExtNeg) rq.addCommonExtendedNegotiation(relSOPClasses.getCommonExtendedNegotiation(cuid)); if (!ts.equals(UID.ExplicitVRLittleEndian)) rq.addPresentationContext(new PresentationContext(rq.getNumberOfPresentationContexts() * 2 + 1, cuid, UID.ExplicitVRLittleEndian)); if (!ts.equals(UID.ImplicitVRLittleEndian)) rq.addPresentationContext(new PresentationContext(rq.getNumberOfPresentationContexts() * 2 + 1, cuid, UID.ImplicitVRLittleEndian)); } rq.addPresentationContext(new PresentationContext(rq.getNumberOfPresentationContexts() * 2 + 1, cuid, ts)); return true; } public Attributes echo() throws IOException, InterruptedException { DimseRSP response = as.cecho(); response.next(); return response.getCommand(); } public void send(final File f, long fmiEndPos, String cuid, String iuid, String filets) throws IOException, InterruptedException, ParserConfigurationException, SAXException { String ts = selectTransferSyntax(cuid, filets); if (f.getName().endsWith(".xml")) { Attributes parsedDicomFile = SAXReader.parse(new FileInputStream(f)); if (CLIUtils.updateAttributes(parsedDicomFile, attrs, uidSuffix)) iuid = parsedDicomFile.getString(Tag.SOPInstanceUID); if (!ts.equals(filets)) { Decompressor.decompress(parsedDicomFile, filets); } as.cstore(cuid, iuid, priority, new DataWriterAdapter(parsedDicomFile), ts, rspHandlerFactory.createDimseRSPHandler(f)); } else { if (uidSuffix == null && attrs.isEmpty() && ts.equals(filets)) { FileInputStream in = new FileInputStream(f); try { in.skip(fmiEndPos); InputStreamDataWriter data = new InputStreamDataWriter(in); as.cstore(cuid, iuid, priority, data, ts, rspHandlerFactory.createDimseRSPHandler(f)); } finally { SafeClose.close(in); } } else { DicomInputStream in = new DicomInputStream(f); try { in.setIncludeBulkData(IncludeBulkData.URI); Attributes data = in.readDataset(-1, -1); if (CLIUtils.updateAttributes(data, attrs, uidSuffix)) iuid = data.getString(Tag.SOPInstanceUID); if (!ts.equals(filets)) { Decompressor.decompress(data, filets); } as.cstore(cuid, iuid, priority, new DataWriterAdapter(data), ts, rspHandlerFactory.createDimseRSPHandler(f)); } finally { SafeClose.close(in); } } } } private String selectTransferSyntax(String cuid, String filets) { Set<String> tss = as.getTransferSyntaxesFor(cuid); if (tss.contains(filets)) return filets; if (tss.contains(UID.ExplicitVRLittleEndian)) return UID.ExplicitVRLittleEndian; return UID.ImplicitVRLittleEndian; } public void close() throws IOException, InterruptedException { if (as != null) { if (as.isReadyForDataTransfer()) as.release(); as.waitForSocketClose(); } } public void open() throws IOException, InterruptedException, IncompatibleConnectionException, GeneralSecurityException { as = ae.connect(remote, rq); } private void onCStoreRSP(Attributes cmd, File f) { int status = cmd.getInt(Tag.Status, -1); switch (status) { case Status.Success: totalSize += f.length(); ++filesSent; System.out.print('.'); break; case Status.CoercionOfDataElements: case Status.ElementsDiscarded: case Status.DataSetDoesNotMatchSOPClassWarning: totalSize += f.length(); ++filesSent; System.err.println(MessageFormat.format(rb.getString("warning"), TagUtils.shortToHexString(status), f)); System.err.println(cmd); break; default: System.out.print('E'); System.err.println(MessageFormat.format(rb.getString("error"), TagUtils.shortToHexString(status), f)); System.err.println(cmd); } } /** * Moved resource bundle into class R Moulton 11/23/15 */ private static class storeSCPResources extends ListResourceBundle { @Override protected Object[][] getContents() { return new Object[][] { { "usage", "storescu [options] -c <aet>@<host>:<port> [<file>..][<directory>..]" }, { "try", "Try `storescu --help' for more information." }, { "description", "\n" + "The storescu application implements a Service Class User (SCU) for the Storage " + "Service Class and for the Verification SOP Class. For each DICOM file on the " + "command line it sends a C-STORE message to a Storage Service Class Provider " + "(SCP) and waits for a response. If no DICOM file is specified, it sends a " + "DICOM C-ECHO message and waits for a response. The application can be used " + "to transmit DICOM images and other DICOM composite objects and to verify " + "basic DICOM connectivity.\n -\n Options:" }, { "example", "-\n" + "Example: storescu -c STORESCP@localhost:11112 image.dcm\n" + "=> Send DICOM image image.dcm to Storage Service Class Provider STORESCP, " + "listening on local port 11112." }, { "rel-ext-neg", "enable SOP Class Relationship Extended Negotiation" }, { "rel-sop-classes", "file path or URL of definition of Related General SOP Classes, " + "resource:rel-sop-classes.properties by default" }, { "set", "specify attributes added to the sent object(s). attr can be specified by " + "keyword or tag value (in hex), e.g. PatientName or 00100010. Attributes in " + "nested Datasets can be specified by including the keyword/tag value of the " + "sequence attribute, e.g. 00400275/00400009 for Scheduled Procedure Step ID in " + "the Request Attributes Sequence. " }, { "uid-suffix", "specify suffix to be appended to the Study, Series and SOP Instance " + "UID of the sent object(s). " }, { "tmp-file-dir", "directory were temporary file with File Meta Information from scanned files is stored; " + "if not specified, the file is stored into the default temporary-file directory" }, { "tmp-file-prefix", "prefix for generated file name for temporary file; 'storescu-' by default" }, { "tmp-file-suffix", "suffix for generated file name for temporary file; '.tmp' by default" }, { "warning", "WARNING: Received C-STORE-RSP with Status {0}H for {1}" }, { "error", "ERROR: Received C-STORE-RSP with Status {0}H for {1}" }, { "scanning", "Scanning files to send" }, { "scanned", "Scanned {0} files in {1}s (={2}ms/file)" }, { "connected", "Connected to {0} in {1}ms" }, { "sent", "Sent {0} objects (={1}MB) in {2}s (={3}MB/s)" }, }; } } }