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}