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) 2012 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.mppsscp; 040 041import java.io.File; 042import java.io.IOException; 043import java.security.GeneralSecurityException; 044import java.util.Properties; 045import java.util.ResourceBundle; 046import java.util.concurrent.ExecutorService; 047import java.util.concurrent.Executors; 048import java.util.concurrent.ScheduledExecutorService; 049 050import org.apache.commons.cli.CommandLine; 051import org.apache.commons.cli.OptionBuilder; 052import org.apache.commons.cli.Options; 053import org.apache.commons.cli.ParseException; 054import org.dcm4che3.data.Tag; 055import org.dcm4che3.data.UID; 056import org.dcm4che3.data.Attributes; 057import org.dcm4che3.data.IOD; 058import org.dcm4che3.data.ValidationResult; 059import org.dcm4che3.io.DicomInputStream; 060import org.dcm4che3.io.DicomOutputStream; 061import org.dcm4che3.net.ApplicationEntity; 062import org.dcm4che3.net.Association; 063import org.dcm4che3.net.Connection; 064import org.dcm4che3.net.Device; 065import org.dcm4che3.net.Status; 066import org.dcm4che3.net.TransferCapability; 067import org.dcm4che3.net.service.BasicCEchoSCP; 068import org.dcm4che3.net.service.BasicMPPSSCP; 069import org.dcm4che3.net.service.DicomServiceException; 070import org.dcm4che3.net.service.DicomServiceRegistry; 071import org.dcm4che3.tool.common.CLIUtils; 072import org.dcm4che3.util.SafeClose; 073import org.slf4j.Logger; 074import org.slf4j.LoggerFactory; 075 076/** 077 * @author Gunter Zeilinger <gunterze@gmail.com> 078 */ 079public class MppsSCP { 080 081 private static ResourceBundle rb = 082 ResourceBundle.getBundle("org.dcm4che3.tool.mppsscp.messages"); 083 084 private static final Logger LOG = LoggerFactory.getLogger(MppsSCP.class); 085 086 private Device device = new Device("mppsscp"); 087 private final ApplicationEntity ae = new ApplicationEntity("*"); 088 private final Connection conn = new Connection(); 089 private File storageDir; 090 private IOD mppsNCreateIOD; 091 private IOD mppsNSetIOD; 092 093 protected final BasicMPPSSCP mppsSCP = new BasicMPPSSCP() { 094 095 @Override 096 protected Attributes create(Association as, Attributes rq, 097 Attributes rqAttrs, Attributes rsp) throws DicomServiceException { 098 return MppsSCP.this.create(as, rq, rqAttrs); 099 } 100 101 @Override 102 protected Attributes set(Association as, Attributes rq, Attributes rqAttrs, 103 Attributes rsp) throws DicomServiceException { 104 return MppsSCP.this.set(as, rq, rqAttrs); 105 } 106 }; 107 private boolean started = false; 108 109 public MppsSCP() throws IOException { 110 device.addConnection(conn); 111 device.addApplicationEntity(ae); 112 ae.setAssociationAcceptor(true); 113 ae.addConnection(conn); 114 DicomServiceRegistry serviceRegistry = new DicomServiceRegistry(); 115 serviceRegistry.addDicomService(new BasicCEchoSCP()); 116 serviceRegistry.addDicomService(mppsSCP); 117 ae.setDimseRQHandler(serviceRegistry); 118 } 119 120 /** 121 * Bind the MPPS SCP to the provided preconfigured ae 122 * 123 * @param applicationEntity 124 */ 125 public MppsSCP(ApplicationEntity applicationEntity) { 126 device = applicationEntity.getDevice(); 127 applicationEntity.setAssociationAcceptor(true); 128 DicomServiceRegistry serviceRegistry = new DicomServiceRegistry(); 129 serviceRegistry.addDicomService(new BasicCEchoSCP()); 130 serviceRegistry.addDicomService(mppsSCP); 131 applicationEntity.setDimseRQHandler(serviceRegistry); 132 } 133 134 private void configure(boolean hasOptionNoValidate, 135 String mppsNCreateIOD, 136 String mppsNSetIOD, 137 boolean ignore, 138 String directory) throws IOException, GeneralSecurityException { 139 configureStorageDirectory(this, ignore, directory); 140 configureIODs(this, hasOptionNoValidate, 141 mppsNCreateIOD, 142 mppsNSetIOD); 143 144 ExecutorService executorService = Executors.newCachedThreadPool(); 145 ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); 146 device.setScheduledExecutor(scheduledExecutorService); 147 device.setExecutor(executorService); 148 } 149 150 public void start() throws IOException, GeneralSecurityException { 151 device.bindConnections(); 152 started = true; 153 } 154 155 public void stop() { 156 157 if (!started) return; 158 159 started = false; 160 161 device.unbindConnections(); 162 ((ExecutorService) device.getExecutor()).shutdown(); 163 device.getScheduledExecutor().shutdown(); 164 165 //very quick fix to block for listening connection 166 while (device.getConnections().get(0).isListening()) 167 { 168 try { 169 Thread.sleep(10); 170 } catch (InterruptedException e) { 171 // ignore 172 } 173 } 174 175 } 176 177 public void setStorageDirectory(File storageDir) { 178 if (storageDir != null) 179 storageDir.mkdirs(); 180 this.storageDir = storageDir; 181 } 182 183 public File getStorageDirectory() { 184 return storageDir; 185 } 186 187 private void setMppsNCreateIOD(IOD mppsNCreateIOD) { 188 this.mppsNCreateIOD = mppsNCreateIOD; 189 } 190 191 private void setMppsNSetIOD(IOD mppsNSetIOD) { 192 this.mppsNSetIOD = mppsNSetIOD; 193 } 194 195 public static void main(String[] args) { 196 try { 197 CommandLine cl = parseComandLine(args); 198 MppsSCP main = new MppsSCP(); 199 200 CLIUtils.configureBindServer(main.conn, main.ae, cl); 201 CLIUtils.configure(main.conn, cl); 202 configureTransferCapability(main.ae, cl); 203 204 main.configure(cl.hasOption("no-validate"), 205 cl.getOptionValue("mpps-ncreate-iod", "resource:mpps-ncreate-iod.xml"), 206 cl.getOptionValue("mpps-nset-iod", "resource:mpps-nset-iod.xml"), 207 cl.hasOption("ignore"), 208 cl.getOptionValue("directory", ".")); 209 210 main.start(); 211 212 } catch (ParseException e) { 213 System.err.println("mppsscp: " + e.getMessage()); 214 System.err.println(rb.getString("try")); 215 System.exit(2); 216 } catch (Exception e) { 217 System.err.println("mppsscp: " + e.getMessage()); 218 e.printStackTrace(); 219 System.exit(2); 220 } 221 } 222 223 private static CommandLine parseComandLine(String[] args) throws ParseException { 224 Options opts = new Options(); 225 CLIUtils.addBindServerOption(opts); 226 CLIUtils.addAEOptions(opts); 227 CLIUtils.addCommonOptions(opts); 228 addStorageDirectoryOptions(opts); 229 addTransferCapabilityOptions(opts); 230 addIODOptions(opts); 231 return CLIUtils.parseComandLine(args, opts, rb, MppsSCP.class); 232 } 233 234 @SuppressWarnings("static-access") 235 private static void addStorageDirectoryOptions(Options opts) { 236 opts.addOption(null, "ignore", false, 237 rb.getString("ignore")); 238 opts.addOption(OptionBuilder 239 .hasArg() 240 .withArgName("path") 241 .withDescription(rb.getString("directory")) 242 .withLongOpt("directory") 243 .create(null)); 244 } 245 246 @SuppressWarnings("static-access") 247 private static void addTransferCapabilityOptions(Options opts) { 248 opts.addOption(OptionBuilder 249 .hasArg() 250 .withArgName("file|url") 251 .withDescription(rb.getString("sop-classes")) 252 .withLongOpt("sop-classes") 253 .create(null)); 254 } 255 256 @SuppressWarnings("static-access") 257 private static void addIODOptions(Options opts) { 258 opts.addOption(null, "no-validate", false, 259 rb.getString("no-validate")); 260 opts.addOption(OptionBuilder 261 .hasArg() 262 .withArgName("file|url") 263 .withDescription(rb.getString("ncreate-iod")) 264 .withLongOpt("ncreate-iod") 265 .create(null)); 266 opts.addOption(OptionBuilder 267 .hasArg() 268 .withArgName("file|url") 269 .withDescription(rb.getString("nset-iod")) 270 .withLongOpt("nset-iod") 271 .create(null)); 272 } 273 274 private static void configureStorageDirectory(MppsSCP main, boolean hasOptionIgnore, String directory) { 275 if (!hasOptionIgnore) { 276 main.setStorageDirectory(new File(directory)); 277 } 278 } 279 280 private static void configureIODs(MppsSCP main, boolean hasOptionNoValidate, String mppsNCreateIOD, String mppsNSetIOD) 281 throws IOException { 282 if (!hasOptionNoValidate) { 283 main.setMppsNCreateIOD(IOD.load(mppsNCreateIOD)); 284 main.setMppsNSetIOD(IOD.load(mppsNSetIOD)); 285 } 286 } 287 288 private static void configureTransferCapability(ApplicationEntity ae, 289 CommandLine cl) throws IOException { 290 Properties p = CLIUtils.loadProperties( 291 cl.getOptionValue("sop-classes", 292 "resource:sop-classes.properties"), 293 null); 294 for (String cuid : p.stringPropertyNames()) { 295 String ts = p.getProperty(cuid); 296 ae.addTransferCapability( 297 new TransferCapability(null, 298 CLIUtils.toUID(cuid), 299 TransferCapability.Role.SCP, 300 CLIUtils.toUIDs(ts))); 301 } 302 } 303 304 private Attributes create(Association as, Attributes rq, Attributes rqAttrs) 305 throws DicomServiceException { 306 if (mppsNCreateIOD != null) { 307 ValidationResult result = rqAttrs.validate(mppsNCreateIOD); 308 if (!result.isValid()) 309 throw DicomServiceException.valueOf(result, rqAttrs); 310 } 311 if (storageDir == null) 312 return null; 313 String cuid = rq.getString(Tag.AffectedSOPClassUID); 314 String iuid = rq.getString(Tag.AffectedSOPInstanceUID); 315 File file = new File(storageDir, iuid); 316 if (file.exists()) 317 throw new DicomServiceException(Status.DuplicateSOPinstance). 318 setUID(Tag.AffectedSOPInstanceUID, iuid); 319 DicomOutputStream out = null; 320 LOG.info("{}: M-WRITE {}", as, file); 321 try { 322 out = new DicomOutputStream(file); 323 out.writeDataset( 324 Attributes.createFileMetaInformation(iuid, cuid, UID.ExplicitVRLittleEndian), rqAttrs); 325 } catch (IOException e) { 326 LOG.warn(as + ": Failed to store MPPS:", e); 327 throw new DicomServiceException(Status.ProcessingFailure, e); 328 } finally { 329 SafeClose.close(out); 330 } 331 return null; 332 } 333 334 private Attributes set(Association as, Attributes rq, Attributes rqAttrs) 335 throws DicomServiceException { 336 if (mppsNSetIOD != null) { 337 ValidationResult result = rqAttrs.validate(mppsNSetIOD); 338 if (!result.isValid()) 339 throw DicomServiceException.valueOf(result, rqAttrs); 340 } 341 if (storageDir == null) 342 return null; 343 String cuid = rq.getString(Tag.RequestedSOPClassUID); 344 String iuid = rq.getString(Tag.RequestedSOPInstanceUID); 345 File file = new File(storageDir, iuid); 346 if (!file.exists()) 347 throw new DicomServiceException(Status.NoSuchObjectInstance). 348 setUID(Tag.AffectedSOPInstanceUID, iuid); 349 LOG.info("{}: M-UPDATE {}", as, file); 350 Attributes data; 351 DicomInputStream in = null; 352 try { 353 in = new DicomInputStream(file); 354 data = in.readDataset(-1, -1); 355 } catch (IOException e) { 356 LOG.warn(as + ": Failed to read MPPS:", e); 357 throw new DicomServiceException(Status.ProcessingFailure, e); 358 } finally { 359 SafeClose.close(in); 360 } 361 if (!"IN PROGRESS".equals(data.getString(Tag.PerformedProcedureStepStatus))) 362 BasicMPPSSCP.mayNoLongerBeUpdated(); 363 364 data.addAll(rqAttrs); 365 DicomOutputStream out = null; 366 try { 367 out = new DicomOutputStream(file); 368 out.writeDataset( 369 Attributes.createFileMetaInformation(iuid, cuid, UID.ExplicitVRLittleEndian), 370 data); 371 } catch (IOException e) { 372 LOG.warn(as + ": Failed to update MPPS:", e); 373 throw new DicomServiceException(Status.ProcessingFailure, e); 374 } finally { 375 SafeClose.close(out); 376 } 377 return null; 378 } 379}