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 ***** */ 038 039package org.dcm4che3.tool.storescp; 040 041import java.io.File; 042import java.io.IOException; 043import java.util.Properties; 044import java.util.ResourceBundle; 045import java.util.concurrent.ExecutorService; 046import java.util.concurrent.Executors; 047import java.util.concurrent.ScheduledExecutorService; 048 049import org.apache.commons.cli.CommandLine; 050import org.apache.commons.cli.OptionBuilder; 051import org.apache.commons.cli.Options; 052import org.apache.commons.cli.ParseException; 053import org.dcm4che3.data.Tag; 054import org.dcm4che3.data.Attributes; 055import org.dcm4che3.data.VR; 056import org.dcm4che3.io.DicomInputStream; 057import org.dcm4che3.io.DicomOutputStream; 058import org.dcm4che3.io.DicomInputStream.IncludeBulkData; 059import org.dcm4che3.net.ApplicationEntity; 060import org.dcm4che3.net.Association; 061import org.dcm4che3.net.Connection; 062import org.dcm4che3.net.Device; 063import org.dcm4che3.net.PDVInputStream; 064import org.dcm4che3.net.Status; 065import org.dcm4che3.net.TransferCapability; 066import org.dcm4che3.net.pdu.PresentationContext; 067import org.dcm4che3.net.service.BasicCEchoSCP; 068import org.dcm4che3.net.service.BasicCStoreSCP; 069import org.dcm4che3.net.service.DicomServiceException; 070import org.dcm4che3.net.service.DicomServiceRegistry; 071import org.dcm4che3.tool.common.CLIUtils; 072import org.dcm4che3.util.AttributesFormat; 073import org.dcm4che3.util.SafeClose; 074import org.slf4j.Logger; 075import org.slf4j.LoggerFactory; 076 077/** 078 * @author Gunter Zeilinger <gunterze@gmail.com> 079 * 080 */ 081public class StoreSCP { 082 083 private static final Logger LOG = LoggerFactory.getLogger(StoreSCP.class); 084 085 private static ResourceBundle rb = 086 ResourceBundle.getBundle("org.dcm4che3.tool.storescp.messages"); 087 private static final String PART_EXT = ".part"; 088 089 private final Device device = new Device("storescp"); 090 private final ApplicationEntity ae = new ApplicationEntity("*"); 091 private final Connection conn = new Connection(); 092 private File storageDir; 093 private AttributesFormat filePathFormat; 094 private int status; 095 private final BasicCStoreSCP cstoreSCP = new BasicCStoreSCP("*") { 096 097 @Override 098 protected void store(Association as, PresentationContext pc, 099 Attributes rq, PDVInputStream data, Attributes rsp) 100 throws IOException { 101 rsp.setInt(Tag.Status, VR.US, status); 102 if (storageDir == null) 103 return; 104 105 String cuid = rq.getString(Tag.AffectedSOPClassUID); 106 String iuid = rq.getString(Tag.AffectedSOPInstanceUID); 107 String tsuid = pc.getTransferSyntax(); 108 File file = new File(storageDir, iuid + PART_EXT); 109 try { 110 storeTo(as, as.createFileMetaInformation(iuid, cuid, tsuid), 111 data, file); 112 renameTo(as, file, new File(storageDir, 113 filePathFormat == null 114 ? iuid 115 : filePathFormat.format(parse(file)))); 116 } catch (Exception e) { 117 deleteFile(as, file); 118 throw new DicomServiceException(Status.ProcessingFailure, e); 119 } 120 } 121 122 }; 123 124 public StoreSCP() throws IOException { 125 device.setDimseRQHandler(createServiceRegistry()); 126 device.addConnection(conn); 127 device.addApplicationEntity(ae); 128 ae.setAssociationAcceptor(true); 129 ae.addConnection(conn); 130 } 131 132 private void storeTo(Association as, Attributes fmi, 133 PDVInputStream data, File file) throws IOException { 134 LOG.info("{}: M-WRITE {}", as, file); 135 file.getParentFile().mkdirs(); 136 DicomOutputStream out = new DicomOutputStream(file); 137 try { 138 out.writeFileMetaInformation(fmi); 139 data.copyTo(out); 140 } finally { 141 SafeClose.close(out); 142 } 143 } 144 145 private static void renameTo(Association as, File from, File dest) 146 throws IOException { 147 LOG.info("{}: M-RENAME {} to {}", as, from, dest); 148 if (!dest.getParentFile().mkdirs()) 149 dest.delete(); 150 if (!from.renameTo(dest)) 151 throw new IOException("Failed to rename " + from + " to " + dest); 152 } 153 154 private static Attributes parse(File file) throws IOException { 155 DicomInputStream in = new DicomInputStream(file); 156 try { 157 in.setIncludeBulkData(IncludeBulkData.NO); 158 return in.readDataset(-1, Tag.PixelData); 159 } finally { 160 SafeClose.close(in); 161 } 162 } 163 164 private static void deleteFile(Association as, File file) { 165 if (file.delete()) 166 LOG.info("{}: M-DELETE {}", as, file); 167 else 168 LOG.warn("{}: M-DELETE {} failed!", as, file); 169 } 170 171 private DicomServiceRegistry createServiceRegistry() { 172 DicomServiceRegistry serviceRegistry = new DicomServiceRegistry(); 173 serviceRegistry.addDicomService(new BasicCEchoSCP()); 174 serviceRegistry.addDicomService(cstoreSCP); 175 return serviceRegistry; 176 } 177 178 public void setStorageDirectory(File storageDir) { 179 if (storageDir != null) 180 storageDir.mkdirs(); 181 this.storageDir = storageDir; 182 } 183 184 public void setStorageFilePathFormat(String pattern) { 185 this.filePathFormat = new AttributesFormat(pattern); 186 } 187 188 public void setStatus(int status) { 189 this.status = status; 190 } 191 192 private static CommandLine parseComandLine(String[] args) 193 throws ParseException { 194 Options opts = new Options(); 195 CLIUtils.addBindServerOption(opts); 196 CLIUtils.addAEOptions(opts); 197 CLIUtils.addCommonOptions(opts); 198 addStatusOption(opts); 199 addStorageDirectoryOptions(opts); 200 addTransferCapabilityOptions(opts); 201 return CLIUtils.parseComandLine(args, opts, rb, StoreSCP.class); 202 } 203 204 @SuppressWarnings("static-access") 205 private static void addStatusOption(Options opts) { 206 opts.addOption(OptionBuilder 207 .hasArg() 208 .withArgName("code") 209 .withDescription(rb.getString("status")) 210 .withLongOpt("status") 211 .create(null)); 212 } 213 214 @SuppressWarnings("static-access") 215 private static void addStorageDirectoryOptions(Options opts) { 216 opts.addOption(null, "ignore", false, 217 rb.getString("ignore")); 218 opts.addOption(OptionBuilder 219 .hasArg() 220 .withArgName("path") 221 .withDescription(rb.getString("directory")) 222 .withLongOpt("directory") 223 .create(null)); 224 opts.addOption(OptionBuilder 225 .hasArg() 226 .withArgName("pattern") 227 .withDescription(rb.getString("filepath")) 228 .withLongOpt("filepath") 229 .create(null)); 230 } 231 232 @SuppressWarnings("static-access") 233 private static void addTransferCapabilityOptions(Options opts) { 234 opts.addOption(null, "accept-unknown", false, 235 rb.getString("accept-unknown")); 236 opts.addOption(OptionBuilder 237 .hasArg() 238 .withArgName("file|url") 239 .withDescription(rb.getString("sop-classes")) 240 .withLongOpt("sop-classes") 241 .create(null)); 242 } 243 244 public static void main(String[] args) { 245 try { 246 CommandLine cl = parseComandLine(args); 247 StoreSCP main = new StoreSCP(); 248 CLIUtils.configureBindServer(main.conn, main.ae, cl); 249 CLIUtils.configure(main.conn, cl); 250 main.setStatus(CLIUtils.getIntOption(cl, "status", 0)); 251 configureTransferCapability(main.ae, cl); 252 configureStorageDirectory(main, cl); 253 ExecutorService executorService = Executors.newCachedThreadPool(); 254 ScheduledExecutorService scheduledExecutorService = 255 Executors.newSingleThreadScheduledExecutor(); 256 main.device.setScheduledExecutor(scheduledExecutorService); 257 main.device.setExecutor(executorService); 258 main.device.bindConnections(); 259 } catch (ParseException e) { 260 System.err.println("storescp: " + e.getMessage()); 261 System.err.println(rb.getString("try")); 262 System.exit(2); 263 } catch (Exception e) { 264 System.err.println("storescp: " + e.getMessage()); 265 e.printStackTrace(); 266 System.exit(2); 267 } 268 } 269 270 private static void configureStorageDirectory(StoreSCP main, CommandLine cl) { 271 if (!cl.hasOption("ignore")) { 272 main.setStorageDirectory( 273 new File(cl.getOptionValue("directory", "."))); 274 if (cl.hasOption("filepath")) 275 main.setStorageFilePathFormat(cl.getOptionValue("filepath")); 276 } 277 } 278 279 private static void configureTransferCapability(ApplicationEntity ae, 280 CommandLine cl) throws IOException { 281 if (cl.hasOption("accept-unknown")) { 282 ae.addTransferCapability( 283 new TransferCapability(null, 284 "*", 285 TransferCapability.Role.SCP, 286 "*")); 287 } else { 288 Properties p = CLIUtils.loadProperties( 289 cl.getOptionValue("sop-classes", 290 "resource:sop-classes.properties"), 291 null); 292 for (String cuid : p.stringPropertyNames()) { 293 String ts = p.getProperty(cuid); 294 TransferCapability tc = new TransferCapability(null, 295 CLIUtils.toUID(cuid), 296 TransferCapability.Role.SCP, 297 CLIUtils.toUIDs(ts)); 298 ae.addTransferCapability(tc); 299 } 300 } 301 } 302 303}