001/** 002 * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ 003 * 004 * This file is part of Dicoogle/dicoogle. 005 * 006 * Dicoogle/dicoogle is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * Dicoogle/dicoogle is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with Dicoogle. If not, see <http://www.gnu.org/licenses/>. 018 */ 019package pt.ua.dicoogle.server; 020 021import pt.ua.dicoogle.core.ServerSettings; 022 023import java.awt.*; 024import java.io.File; 025import java.io.IOException; 026import java.net.URI; 027import java.util.*; 028import java.util.List; 029import java.util.concurrent.*; 030 031import org.dcm4che2.data.DicomObject; 032import org.dcm4che2.data.Tag; 033import org.dcm4che2.data.UID; 034import org.dcm4che2.net.Association; 035import org.dcm4che2.net.CommandUtils; 036import org.dcm4che2.net.Device; 037import org.dcm4che2.net.DicomServiceException; 038 039///import org.dcm4che2.net.Executor; 040/** dcm4che doesn't support Executor anymore, so now import from java.util */ 041import org.slf4j.Logger; 042import org.slf4j.LoggerFactory; 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045 046import org.dcm4che2.net.NetworkApplicationEntity; 047import org.dcm4che2.net.NetworkConnection; 048import org.dcm4che2.net.NewThreadExecutor; 049import org.dcm4che2.net.PDVInputStream; 050import org.dcm4che2.net.Status; 051import org.dcm4che2.net.TransferCapability; 052import org.dcm4che2.net.service.StorageService; 053import org.dcm4che2.net.service.VerificationService; 054 055 056import pt.ua.dicoogle.plugins.PluginController; 057import pt.ua.dicoogle.sdk.IndexerInterface; 058import pt.ua.dicoogle.sdk.StorageInterface; 059import pt.ua.dicoogle.sdk.datastructs.Report; 060 061 062/** 063 * DICOM Storage Service is provided by this class 064 * @author Marco Pereira 065 */ 066 067public class RSIStorage extends StorageService 068{ 069 070 private SOPList list; 071 private ServerSettings settings; 072 073 private Executor executor = new NewThreadExecutor("RSIStorage"); 074 private Device device = new Device("RSIStorage"); 075 private NetworkApplicationEntity nae = new NetworkApplicationEntity(); 076 private NetworkConnection nc = new NetworkConnection(); 077 078 private String path; 079 private DicomDirCreator dirc; 080 081 private int fileBufferSize = 256; 082 private int threadPoolSize = 10; 083 084 private ExecutorService pool = Executors.newFixedThreadPool(threadPoolSize); 085 086 private boolean gzip = ServerSettings.getInstance().isGzipStorage();; 087 088 private Set<String> alternativeAETs = new HashSet<>(); 089 private Set<String> priorityAETs = new HashSet<>(); 090 091 // Changed to support priority queue. 092 private BlockingQueue<ImageElement> queue = new PriorityBlockingQueue<ImageElement>(); 093 private NetworkApplicationEntity[] naeArr = null; 094 095 /** 096 * 097 * @param Services List of supported SOP Classes 098 * @param l list of Supported SOPClasses with supported Transfer Syntax 099 * @param s Server Settings for this execution of the storage service 100 */ 101 102 public RSIStorage(String [] Services, SOPList l) 103 { 104 //just because the call to super must be the first instruction 105 super(Services); 106 107 //our configuration format 108 list = l; 109 settings = ServerSettings.getInstance(); 110 111 // Added default alternative AETitle. 112 alternativeAETs.add(ServerSettings.getInstance().getNodeName()); 113 114 path = settings.getPath(); 115 if (path == null) { 116 path = "/dev/null"; 117 } 118 119 this.priorityAETs = settings.getPriorityAETitles(); 120 LoggerFactory.getLogger(RSIStorage.class).debug("Priority C-STORE: " + this.priorityAETs); 121 122 device.setNetworkApplicationEntity(nae); 123 124 device.setNetworkConnection(nc); 125 nae.setNetworkConnection(nc); 126 127 //we accept assoociations, this is a server 128 nae.setAssociationAcceptor(true); 129 //we support the VerificationServiceSOP 130 nae.register(new VerificationService()); 131 //and the StorageServiceSOP 132 nae.register(this); 133 134 nae.setAETitle(settings.getAE()); 135 136 137 nc.setPort(settings.getStoragePort()); 138 139 140 this.nae.setInstalled(true); 141 this.nae.setAssociationAcceptor(true); 142 this.nae.setAssociationInitiator(false); 143 144 145 ServerSettings s = ServerSettings.getInstance(); 146 this.nae.setDimseRspTimeout(60000*300); 147 this.nae.setIdleTimeout(60000*300); 148 this.nae.setMaxPDULengthReceive(s.getMaxPDULengthReceive()+1000); 149 this.nae.setMaxPDULengthSend(s.getMaxPDULenghtSend()+1000); 150 this.nae.setRetrieveRspTimeout(60000*300); 151 152 153 // Added alternative AETitles. 154 155 naeArr = new NetworkApplicationEntity[alternativeAETs.size()+1]; 156 // Just adding the first AETitle 157 naeArr[0] = nae; 158 159 int k = 1 ; 160 161 for (String alternativeAET: alternativeAETs) 162 { 163 NetworkApplicationEntity nae2 = new NetworkApplicationEntity(); 164 nae2.setNetworkConnection(nc); 165 nae2.setDimseRspTimeout(60000*300); 166 nae2.setIdleTimeout(60000*300); 167 nae2.setMaxPDULengthReceive(s.getMaxPDULengthReceive()+1000); 168 nae2.setMaxPDULengthSend(s.getMaxPDULenghtSend()+1000); 169 nae2.setRetrieveRspTimeout(60000*300); 170 //we accept assoociations, this is a server 171 nae2.setAssociationAcceptor(true); 172 //we support the VerificationServiceSOP 173 nae2.register(new VerificationService()); 174 //and the StorageServiceSOP 175 nae2.register(this); 176 nae2.setAETitle(alternativeAET); 177 ServerSettings settings = ServerSettings.getInstance(); 178 String[] array = settings.getCAET(); 179 180 if (array != null) 181 { 182 nae2.setPreferredCallingAETitle(settings.getCAET()); 183 } 184 naeArr[k] = nae2; 185 k++; 186 187 } 188 189 // Just set the Network Application Entity array - which accepts a set of AEs. 190 device.setNetworkApplicationEntity(naeArr); 191 192 193 194 initTS(Services); 195 } 196 /** 197 * Sets the tranfer capability for this execution of the storage service 198 * @param Services Services to be supported 199 */ 200 private void initTS(String [] Services) 201 { 202 int count = list.getAccepted(); 203 //System.out.println(count); 204 TransferCapability[] tc = new TransferCapability[count + 1]; 205 String [] Verification = {UID.ImplicitVRLittleEndian, UID.ExplicitVRLittleEndian, UID.ExplicitVRBigEndian}; 206 String [] TS; 207 TransfersStorage local; 208 209 tc[0] = new TransferCapability(UID.VerificationSOPClass, Verification, TransferCapability.SCP); 210 int j = 0; 211 for (int i = 0; i < Services.length; i++) 212 { 213 count = 0; 214 local = list.getTS(Services[i]); 215 if (local.getAccepted()) 216 { 217 TS = local.getVerboseTS(); 218 if(TS != null) 219 { 220 221 tc[j+1] = new TransferCapability(Services[i], TS, TransferCapability.SCP); 222 j++; 223 } 224 } 225 } 226 227 // Setting the TS in all NetworkApplicationEntitys 228 for (int i = 0 ; i<naeArr.length;i++) 229 { 230 231 naeArr[i].setTransferCapability(tc); 232 } 233 nae.setTransferCapability(tc); 234 } 235 236 @Override 237 /** 238 * Called when a C-Store Request has been accepted 239 * Parameters defined by dcm4che2 240 */ 241 public void cstore(final Association as, final int pcid, DicomObject rq, PDVInputStream dataStream, String tsuid) throws DicomServiceException, IOException 242 { 243 //DebugManager.getInstance().debug(":: Verify Permited AETs @??C-Store Request "); 244 245 boolean permited = false; 246 247 if(ServerSettings.getInstance().getPermitAllAETitles()){ 248 permited = true; 249 } 250 else { 251 String permitedAETs[] = ServerSettings.getInstance().getCAET(); 252 253 for (int i = 0; i < permitedAETs.length; i++) { 254 if (permitedAETs[i].equals(as.getCallingAET())) { 255 permited = true; 256 break; 257 } 258 } 259 } 260 261 if (!permited) { 262 //DebugManager.getInstance().debug("Client association NOT permited: " + as.getCallingAET() + "!"); 263 System.err.println("Client association NOT permited: " + as.getCallingAET() + "!"); 264 as.abort(); 265 266 return; 267 } else { 268 //DebugManager.getInstance().debug("Client association permited: " + as.getCallingAET() + "!"); 269 System.err.println("Client association permited: " + as.getCallingAET() + "!"); 270 } 271 272 final DicomObject rsp = CommandUtils.mkRSP(rq, CommandUtils.SUCCESS); 273 onCStoreRQ(as, pcid, rq, dataStream, tsuid, rsp); 274 as.writeDimseRSP(pcid, rsp); 275 //onCStoreRSP(as, pcid, rq, dataStream, tsuid, rsp); 276 } 277 278 @Override 279 /** 280 * Actually do the job of saving received file on disk 281 * on this server with extras such as Lucene indexing 282 * and DICOMDIR update 283 */ 284 protected void onCStoreRQ(Association as, int pcid, DicomObject rq, PDVInputStream dataStream, String tsuid, DicomObject rsp) throws IOException, DicomServiceException 285 { 286 try 287 { 288 289 String cuid = rq.getString(Tag.AffectedSOPClassUID); 290 String iuid = rq.getString(Tag.AffectedSOPInstanceUID); 291 292 DicomObject d = dataStream.readDataset(); 293 294 d.initFileMetaInformation(cuid, iuid, tsuid); 295 296 Iterable <StorageInterface> plugins = PluginController.getInstance().getStoragePlugins(true); 297 298 URI uri = null; 299 for (StorageInterface storage : plugins) 300 { 301 uri = storage.store(d); 302 if(uri != null) { 303 // queue to index 304 ImageElement element = new ImageElement(); 305 element.setCallingAET(as.getCallingAET()); 306 element.setUri(uri); 307 queue.add(element); 308 } 309 } 310 311 } catch (IOException e) { 312 throw new DicomServiceException(rq, Status.ProcessingFailure, e.getMessage()); 313 } 314 } 315 316 /** 317 * ImageElement is a entry of a C-STORE. For Each C-STORE RQ 318 * an ImageElement is created and are put in the queue to index. 319 * 320 * This only happens after the store in Storage Plugins. 321 * 322 * @param <E> 323 */ 324 class ImageElement<E extends Comparable<? super E>> 325 implements Comparable<ImageElement<E>>{ 326 private URI uri; 327 private String callingAET; 328 329 public URI getUri() { 330 return uri; 331 } 332 333 public void setUri(URI uri) { 334 this.uri = uri; 335 } 336 337 public String getCallingAET() { 338 return callingAET; 339 } 340 341 public void setCallingAET(String callingAET) { 342 this.callingAET = callingAET; 343 } 344 345 @Override 346 public int compareTo(ImageElement<E> o1) { 347 if (o1.getCallingAET().equals(this.getCallingAET())) 348 return 0 ; 349 else if (settings.getPriorityAETitles().contains(this.getCallingAET())) 350 return -1; 351 else return 1; 352 } 353 } 354 355 356 class Indexer extends Thread 357 { 358 public Collection<IndexerInterface> plugins; 359 360 public void run() 361 { 362 while (true) 363 { 364 try 365 { 366 // Fetch an element by the queue taking into account the priorities. 367 ImageElement element = queue.take(); 368 URI exam = element.getUri(); 369 if(exam != null) 370 { 371 List <Report> reports = PluginController.getInstance().indexBlocking(exam); 372 } 373 } catch (InterruptedException ex) { 374 LoggerFactory.getLogger(RSIStorage.class).error(ex.getMessage(), ex); 375 } 376 377 } 378 379 } 380 } 381 382 383 private String getFullPath(DicomObject d) 384 { 385 386 return getDirectory(d) + File.separator + getBaseName(d); 387 388 } 389 390 391 private String getFullPathCache(String dir, DicomObject d) 392 { 393 return dir + File.separator + getBaseName(d); 394 395 } 396 397 398 399 private String getBaseName(DicomObject d) 400 { 401 String result = "UNKNOWN.dcm"; 402 String sopInstanceUID = d.getString(Tag.SOPInstanceUID); 403 return sopInstanceUID+".dcm"; 404 } 405 406 407 private String getDirectory(DicomObject d) 408 { 409 410 String result = "UN"; 411 412 String institutionName = d.getString(Tag.InstitutionName); 413 String modality = d.getString(Tag.Modality); 414 String studyDate = d.getString(Tag.StudyDate); 415 String accessionNumber = d.getString(Tag.AccessionNumber); 416 String studyInstanceUID = d.getString(Tag.StudyInstanceUID); 417 String patientName = d.getString(Tag.PatientName); 418 419 if (institutionName==null || institutionName.equals("")) 420 { 421 institutionName = "UN_IN"; 422 } 423 institutionName = institutionName.trim(); 424 institutionName = institutionName.replace(" ", ""); 425 institutionName = institutionName.replace(".", ""); 426 institutionName = institutionName.replace("&", ""); 427 428 429 if (modality == null || modality.equals("")) 430 { 431 modality = "UN_MODALITY"; 432 } 433 434 if (studyDate == null || studyDate.equals("")) 435 { 436 studyDate = "UN_DATE"; 437 } 438 else 439 { 440 try 441 { 442 String year = studyDate.substring(0, 4); 443 String month = studyDate.substring(4, 6); 444 String day = studyDate.substring(6, 8); 445 446 studyDate = year + File.separator + month + File.separator + day; 447 448 } 449 catch(Exception e) 450 { 451 e.printStackTrace(); 452 studyDate = "UN_DATE"; 453 } 454 } 455 456 if (accessionNumber == null || accessionNumber.equals("")) 457 { 458 patientName = patientName.trim(); 459 patientName = patientName.replace(" ", ""); 460 patientName = patientName.replace(".", ""); 461 patientName = patientName.replace("&", ""); 462 463 if (patientName == null || patientName.equals("")) 464 { 465 if (studyInstanceUID == null || studyInstanceUID.equals("")) 466 { 467 accessionNumber = "UN_ACC"; 468 } 469 else 470 { 471 accessionNumber = studyInstanceUID; 472 } 473 } 474 else 475 { 476 accessionNumber = patientName; 477 478 } 479 480 } 481 482 result = path+File.separator+institutionName+File.separator+modality+File.separator+studyDate+File.separator+accessionNumber; 483 484 return result; 485 486 } 487 private Indexer indexer = new Indexer(); 488 /* 489 * Start the Storage Service 490 * @throws java.io.IOException 491 */ 492 public void start() throws IOException 493 { 494 //dirc = new DicomDirCreator(path, "Dicoogle"); 495 pool = Executors.newFixedThreadPool(threadPoolSize); 496 device.startListening(executor); 497 indexer.start(); 498 499 500 } 501 502 /** 503 * Stop the storage service 504 */ 505 public void stop() 506 { 507 this.pool.shutdown(); 508 try { 509 pool.awaitTermination(6, TimeUnit.DAYS); 510 } catch (InterruptedException ex) { 511 LoggerFactory.getLogger(RSIStorage.class).error(ex.getMessage(), ex); 512 } 513 device.stopListening(); 514 515 //dirc.dicomdir_close(); 516 } 517}