source: trunk/grails-app/controllers/nl/tno/massSequencing/files/ImportController.groovy @ 72

Last change on this file since 72 was 72, checked in by robert@…, 8 years ago

Implemented addition of logfiles to sequence data

File size: 19.9 KB
Line 
1package nl.tno.massSequencing.files
2
3import org.codehaus.groovy.grails.commons.ConfigurationHolder
4import org.hibernate.SessionFactory
5import grails.converters.*;
6import nl.tno.massSequencing.*
7
8class ImportController {
9        def fileService
10        def fastaService
11        def importService
12        def classificationService
13        def sessionFactory
14        def workerService
15       
16        /**************************************************************************
17         *
18         * Methods for handling uploaded sequence, quality and classification files
19         *
20         *************************************************************************/
21
22        /**
23         * Shows a screen to indicate that files are being parsed
24         */
25        def parseUploadedFiles = {
26                def entityType = params.entityType
27                def entityId = params.id
28               
29                // Check whether files are given
30                def names = [] + params.list( 'sequencefiles' )
31
32                if( !names ) {
33                        flash.message = "No files uploaded for processing"
34                        if( params.entityType && params.id)
35                                redirect( controller: params.entityType, action: 'show', 'id': params.id)
36                        else
37                                redirect( url: "" )
38                               
39                        return
40                }
41               
42                // Check for total size of the files in order to be able
43                // to show a progress bar
44                long filesize = 0;
45                names.each {
46                        filesize += fileService.get( it )?.length()
47                }
48
49                // Create a unique process identifier
50                String processId = workerService.initProcess( session, "Parsing files", 2, filesize ); 
51                                       
52                session.process[ processId ].filenames = names;
53                session.process[ processId ].entityId = entityId;
54                session.process[ processId ].entityType = entityType
55               
56                // Retrieve worker URL
57                def finishUrl = createLink( controller: "import", action: 'parseUploadResult', params: [ processId: processId ] ).toString();
58                def returnUrl = createLink( controller: entityType, action: "show", id: entityId ).toString();
59               
60                def url = workerService.startProcess( session, processId, finishUrl, returnUrl )
61               
62                //
63                // Initiate work
64                //
65               
66                /* Parses uploaded files, discards files we can not handle
67                *
68                * [
69                *               success: [
70                *                       [filename: 'abc.fasta', type: FASTA, numSequences: 190]
71                *                       [filename: 'cde.fasta', type: FASTA, numSequences: 140]
72                *                       [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38]
73                *                       [filename: 'cde.qual', type: QUAL, numSequences: 140, avgQuality: 29]
74                *               ],
75                *               failure: [
76                *                       [filename: 'testing.doc', message: 'Type not recognized']
77                *               ]
78                * ]
79                *
80                * The second parameter is a callback function to update progress indicators
81                */
82           def httpSession = session;
83           def onProgress = { progress, total ->
84                   // Update progress
85                   httpSession.progress[ processId ].stepTotal = total;
86                   httpSession.progress[ processId ].stepProgress = progress;
87           }
88           def newStep = { total, description ->
89                   // Start a new step
90                   httpSession.progress[ processId ].stepTotal = total;
91                   httpSession.progress[ processId ].stepProgress = 0;
92                   
93                   httpSession.progress[ processId ].stepDescription = description;
94                   httpSession.progress[ processId ].stepNum++;
95           }
96
97           // Perform the actual computations asynchronously
98           runAsync {
99                   def entity
100                   
101                   // Determine entity and assaysamples
102                   switch( httpSession.process[ processId ].entityType ) {
103                           case "run":
104                                   entity = getRun( httpSession.process[ processId ].entityId );
105                                   break;
106                           case "assay":
107                                   entity = getAssay( httpSession.process[ processId ].entityId );
108                                   break;
109                           default:
110                                   httpSession.progress[ processId ].error = true;
111                                   httpSession.progress[ processId ].finished = true;
112                                   return;
113                   }
114                   
115                   if (!entity) {
116                           httpSession.progress[ processId ].error = true;
117                           httpSession.progress[ processId ].finished = true;
118                           return;
119                   }
120   
121                   def assaySamples = entity.assaySamples.findAll { it.assay.study.canWrite( httpSession.user ) };
122                   
123                   def parsedFiles = importService.parseFiles( names, onProgress, [progress: 0, total: httpSession.progress[ processId ].stepTotal ], newStep );
124                   
125                   // Determine excel matches from the uploaded files
126                   parsedFiles.success = fastaService.inferExcelMatches( parsedFiles.success );
127                   
128                   // Match files with samples in the database
129                   def matchedFiles = fastaService.matchFiles( parsedFiles.success, assaySamples );
130   
131                   // Sort files on filename
132                   matchedFiles.sort { a,b -> a.fasta?.originalfilename <=> b.fasta?.originalfilename }
133   
134                   // Retrieve all files that have not been matched
135                   def notMatchedFiles = parsedFiles.success.findAll {
136                           switch( it.type ) {
137                                   case "fasta":
138                                           return !matchedFiles*.fasta*.filename.contains( it.filename );
139                                   case "qual":
140                                           return !matchedFiles*.feasibleQuals.flatten().filename.contains( it.filename );
141                                   case "taxonomy":
142                                           return !matchedFiles*.feasibleClassifications.flatten().filename.contains( it.filename );
143                           }
144                           return false;
145                   }
146                   
147                   // Saved file matches in session to use them later on
148                   httpSession.process[ processId ].processedFiles = [ parsed: parsedFiles,  matched: matchedFiles, notMatched: notMatchedFiles ];
149                   
150                   // Check whether quality, classification or logfiles have been added
151                   def types = [ "fasta", "qual", "taxonomy", "logfile" ];
152                   def typesExist = [:]
153                   types.each { type -> typesExist[ type ] = parsedFiles.success.any { it.type == type } }
154                   
155                   httpSession.process[ processId ].fileTypes = typesExist
156                   
157                   // Tell the frontend we are finished
158                   httpSession.progress[ processId ].finished = true;
159           }
160               
161                redirect( url: url );
162        }
163       
164        /**
165         * Show result of processing uploaded files (step 1)
166         */
167        def parseUploadResult = {
168                def processId = params.processId;
169                // load study with id specified by param.id
170                def entity
171               
172                switch( session.process[ processId ].entityType ) {
173                        case "run":
174                                entity = getRun( session.process[ processId ].entityId )
175                                break;
176                        case "assay":
177                                entity = getAssay( session.process[ processId ].entityId )
178                                break;
179                        default:
180                                response.setStatus( 404, "No entity found" );
181                                render "";
182                                return;
183                }
184               
185                def assaySamples = entity.assaySamples.findAll { it.assay.study.canWrite( session.user ) };
186               
187                if (!entity) {
188                        response.setStatus( 404, flash.error )
189                        render "";
190                        return
191                }
192               
193                if( !session.process[ processId ].processedFiles ) {
194                        flash.error = "Processing of files failed. Maybe the session timed out."
195                        redirect( controller: params.entityType, action: 'show', 'id': params.id)
196                        return
197                }
198               
199                // Find matching sequenceData objects for taxonomyfiles that have not been matched
200                def notMatchedFiles = session.process[ processId ].processedFiles.notMatched;
201                def extraClassifications = notMatchedFiles.findAll { it.type == "taxonomy" }
202                extraClassifications.collect {
203                        // Find all sequence files that have the correct number of sequences and are in the list of assaySamples
204                        it[ 'feasibleSequenceData' ] = SequenceData.findAllByNumSequences( it.numLines ).findAll { assaySamples.contains( it.sample ) }
205                        return it
206                }
207               
208                [       entityType: session.process[ processId ].entityType, processId: processId, entity: entity, 
209                        parsedFiles: session.process[ processId ].processedFiles.parsed, 
210                        matchedFiles: session.process[ processId ].processedFiles.matched, 
211                        remainingClassificationFiles: extraClassifications,
212                        existingTypes: session.process[ processId ].fileTypes,
213                        selectedRun: params.selectedRun ]
214        }
215
216        /**
217         * Returns from the upload wizard without saving the data. The uploaded files are removed
218         */
219        def returnWithoutSaving = {
220                def processId = params.processId;
221                def entityType = session.process[ processId ].entityType;
222                def entityId = session.process[ processId ].entityId;
223               
224                // Delete all uploaded files from disk
225                session.process[ processId ]?.processedFiles?.parsed?.success?.each {
226                        fileService.delete( it.filename );
227                }
228               
229                // Clear process from session
230                workerService.clearProcess( session, processId );
231
232                // Redirect to the correct controller           
233                switch( entityType ) {
234                        case "run":
235                        case "assay":
236                                redirect( controller: entityType, action: "show", id: entityId );
237                                return;
238                        default:
239                                response.setStatus( 404, "No entity found" );
240                                render "";
241                                return;
242                }
243               
244               
245        }
246       
247        /**
248         * Shows a screen with the progress of saving matched files
249         */
250        def saveMatchedFiles = {
251                def processId = params.processId
252
253                def entityType = session.process[ processId ].entityType
254                def entityId = session.process[ processId ].entityId
255               
256                session.process[ processId ].matchedFiles = params.file
257                session.process[ processId ].matchedRemainingClassification = params.remainingClassification
258               
259                // Check for total size of the classification files in order to be able
260                // to show a progress bar. The handling of classification files is orders
261                // of magnitude bigger than the rest, so we only show progress of those files
262                long filesize = 0;
263
264                // Loop through all files. Those are the numeric elements in the 'files' array
265                def digitRE = ~/^\d+$/;
266                params.file.findAll { it.key.matches( digitRE ) }.each { file ->
267                        def filevalue = file.value;
268                       
269                        // Check if the file is selected
270                        if( filevalue.include == "on" ) {
271                                if( fileService.fileExists( filevalue.fasta ) ) {
272                                        // Also save classification data for this file, if it is present
273                                        if( filevalue.classification ) {
274                                                filesize += fileService.get( filevalue.classification )?.size()
275                                        }
276                                }
277                        }
278                }
279                params.remainingClassification.findAll { it.key.matches( digitRE ) }.each { file ->
280                        def filevalue = file.value;
281                       
282                        // Check if the file is selected
283                        if( filevalue.include == "on" ) {
284                                if( fileService.fileExists( filevalue.filename ) ) {
285                                        // Also save classification data for this file, if it is present
286                                        filesize += fileService.get( filevalue.filename )?.size()
287                                }
288                        }
289                }
290
291                // Clear old process, but save useful data
292                def processInfo = session.process[ processId ]
293                workerService.clearProcess( session, processId );
294               
295                // Create a new unique process identifier
296                processId = workerService.initProcess( session, "Store sequence data and classification", 2, filesize );
297               
298                session.progress[ processId ].stepNum = 2;
299                session.process[ processId ] = processInfo;
300               
301                // Retrieve worker URL
302                def finishUrl = createLink( controller: "import", action: 'saveMatchedResult', params: [ processId: processId ] ).toString();
303                def returnUrl = createLink( controller: entityType, action: "show", entityId ).toString();
304               
305                def url = workerService.startProcess( session, processId, finishUrl, returnUrl )
306               
307                //
308                // Initiate work
309                //
310                // Check whether files are given
311                def files = session.process[ processId ].matchedFiles
312                def remainingClassification = session.process[ processId ].matchedRemainingClassification;
313               
314                if( !files && !remainingClassification ) {
315                        flash.message = "No files were selected for import."
316                        redirect( controller: session.process[ processId ].entityType, action: 'show', 'id': session.process[ processId ].entityId)
317                        return
318                }
319
320                File permanentDir = fileService.absolutePath( ConfigurationHolder.config.massSequencing.fileDir )
321               
322                // This closure enables keeping track of the progress
323                def httpSession = session;
324                def onProgress = { progress ->
325                        // Update progress
326                        httpSession.progress[ processId ].stepProgress += progress;
327                }
328               
329                // Run the computations asynchronously, since it takes a lot of time
330                runAsync {
331                        // Loop through all FASTA files. Those are the numeric elements in the 'files' array
332                        def fastaReturn = saveMatchedFastaFiles( files, httpSession.process[ processId ]?.processedFiles, onProgress );
333                        def classificationReturn = saveRemainingClassificationFiles( remainingClassification, onProgress );
334                       
335                        // Update classification (summary) for updated samples
336                        def samplesClassified = [] + fastaReturn.samplesClassified + classificationReturn.samplesClassified;
337                        classificationService.updateClassificationForAssaySamples( samplesClassified.findAll { it }.unique() )
338                       
339                        def returnStructure = [
340                                numSequenceFiles: fastaReturn.numSequenceFiles,
341                                numQualFiles: fastaReturn.numQualFiles,
342                                numClassificationFiles: fastaReturn.numClassificationFiles,
343                                numLogFiles: fastaReturn.numLogFiles,
344                                numExtraClassificationFiles: classificationReturn.numExtraClassifications,
345                                numTotal: fastaReturn.numSequenceFiles + classificationReturn.numExtraClassifications,
346                                errors: [] + fastaReturn.errors + classificationReturn.errors
347                        ]
348                       
349                        // Return all files that have not been moved
350                        httpSession.process[ processId ]?.processedFiles?.parsed?.success?.each {
351                                fileService.delete( it.filename );
352                        }
353                       
354                        httpSession.process[ processId ].result = returnStructure;
355                       
356                        // Tell the frontend we are finished
357                        httpSession.progress[ processId ].finished = true;
358       
359                }
360               
361                redirect( url: url );
362        }
363       
364        def saveMatchedFastaFiles( def files, processedFiles, Closure onProgress ) {
365                int numSuccesful = 0;
366                int numQualFiles = 0;
367                int numClassificationFiles = 0;
368                int numLogFiles = 0;
369                def samplesClassified = [];
370                def errors = [];
371
372                def digitRE = ~/^\d+$/;
373                files.findAll { it.key.matches( digitRE ) }.each { file ->
374                        def filevalue = file.value;
375                       
376                        // Check if the file is selected
377                        if( filevalue.include == "on" ) {
378                                if( fileService.fileExists( filevalue.fasta ) ) {
379                                        try {
380                                                def permanent = fastaService.savePermanent( filevalue.fasta, filevalue.qual, filevalue.logfile, processedFiles );
381                                               
382                                                // Save the data into the database
383                                                SequenceData sd = new SequenceData();
384                                               
385                                                sd.sequenceFile = permanent.fasta
386                                                sd.qualityFile = permanent.qual
387                                                sd.logFile = permanent.logfile
388                                                sd.numSequences = permanent.numSequences
389                                                sd.averageQuality = permanent.avgQuality
390                                                       
391                                                def sample = AssaySample.get( filevalue.assaySample );
392                                                if( sample ) {
393                                                        sample.addToSequenceData( sd );
394                                                       
395                                                        AssaySample.recalculateNumSequences( sample );
396                                                }
397                                               
398                                                if( !sd.validate() ) {
399                                                        errors << "an error occurred while saving " + filevalue.fasta + ": validation of SequenceData failed.";
400                                                } else {
401                                                        sd.save(flush:true);
402                                                       
403                                                        // Also save classification data for this file, if it is present
404                                                        if( filevalue.classification ) {
405                                                                classificationService.storeClassification( filevalue.classification, sd, onProgress )
406                                                                samplesClassified << sample
407                                                               
408                                                                numClassificationFiles++;
409                                                        }
410                                                       
411                                                        if( sd.qualityFile )
412                                                                numQualFiles++;
413                                                       
414                                                        if( sd.logFile )
415                                                                numLogFiles++;
416                                                       
417                                                        numSuccesful++;
418                                                }
419                                        } catch( Exception e ) {
420                                                e.printStackTrace();
421                                                errors << "an error occurred while saving " + filevalue.fasta + ": " + e.getMessage()
422                                        }
423                                }
424                        } else {
425                                // File doesn't need to be included in the system. Delete it also from disk
426                                fileService.delete( filevalue.fasta );
427                        }
428                }
429               
430                return [ numSequenceFiles: numSuccesful, numQualFiles: numQualFiles, numClassificationFiles: numClassificationFiles, numLogFiles: numLogFiles, errors: errors, samplesClassified: samplesClassified.unique() ]
431        }
432       
433        def saveRemainingClassificationFiles( def files, Closure onProgress ) {
434                def digitRE = ~/^\d+$/;
435                def errors = [];
436                def samplesClassified = [];
437                def numSuccesful = 0;
438               
439                files.findAll { it.key.matches( digitRE ) }.each { file ->
440                        def filevalue = file.value;
441                       
442                        // Check if the file is selected
443                        if( filevalue.include == "on" ) {
444                                if( fileService.fileExists( filevalue.filename ) ) {
445                                        def sequenceDataId = filevalue.sequenceData;
446                                        try {
447                                                if( sequenceDataId.toString().isLong() ) {
448                                                        // Retrieve sequenceData and sample now, because the session will be cleared during import
449                                                        def sequenceData = SequenceData.get( sequenceDataId.toString().toLong() );
450                                                        def sample = sequenceData.sample;
451                                                       
452                                                        if( sequenceData ) {
453                                                                classificationService.removeClassificationForSequenceData( sequenceData );
454                                                                classificationService.storeClassification( filevalue.filename, sequenceData, onProgress )
455                                                                samplesClassified << sample;
456                                                        }
457       
458                                                        numSuccesful++;
459                                                } else {
460                                                        errors << "a wrong ID is entered for classification file " + filevalue.filename;
461                                                }
462                                        } catch( Exception e ) {
463                                                e.printStackTrace();
464                                                errors << "an error occurred while saving " + filevalue.filename + ": " + e.getMessage()
465                                        }
466                                }
467                        }
468                       
469                        // File doesn't need to be included in the system. Delete it from disk.
470                        fileService.delete( filevalue.filename );
471                }
472               
473                return [ numExtraClassifications: numSuccesful, errors: errors, samplesClassified: samplesClassified.unique()  ]
474               
475        }
476
477        /**
478         * Redirects the user back to the start screen with a message about how things went
479         */
480        def saveMatchedResult = {
481                def processId = params.processId
482               
483                def result = session.process[ processId ].result 
484
485                // Return a message to the user
486                if( result.numTotal == 0 ) {
487                       
488                        if( result.errors.size() > 0 ) {
489                                flash.error = "None of the files were imported, because "
490                                result.errors.each {
491                                        flash.error += "<br />- " + it
492                                }
493                        } else {
494                                flash.message = "None of the files were imported, because none of the files were selected for import."
495                        }
496                } else {
497                        flash.message = ""     
498                        if( result.numSequenceFiles == 1 ) {
499                                flash.message += result.numSequenceFiles + " sequence file has been added to the system"
500                        } else if( result.numSequenceFiles > 1 ) {
501                                flash.message += result.numSequenceFiles + " sequence files have been added to the system"
502                        }
503                       
504                        if( result.numQualFiles > 0 || result.numClassificationFiles > 0 || result.numLogFiles > 0 ) {
505                                flash.message += ", with";
506                        }
507                       
508                        if( result.numQualFiles == 1 ) {
509                                flash.message += " 1 quality file"
510                        } else if( result.numQualFiles > 1 ) {
511                                flash.message += " " + result.numQualFiles + " quality files"
512                        }
513                       
514                        if( result.numQualFiles > 0 && ( result.numClassificationFiles > 0 || result.numLogFiles > 0 ) ) {
515                                flash.message += " and";
516                        }
517                       
518                        if( result.numClassificationFiles == 1 ) {
519                                flash.message += " 1 classification file"
520                        } else if( result.numClassificationFiles > 1 ) {
521                                flash.message += " " + result.numClassificationFiles + " classification files"
522                        }
523
524                        if( ( result.numQualFiles > 0 || result.numClassificationFiles > 0 ) && result.numLogFiles > 0 ) {
525                                flash.message += " and";
526                        }
527                       
528                        if( result.numLogFiles == 1 ) {
529                                flash.message += " 1 log file"
530                        } else if( result.numLogFiles > 1 ) {
531                                flash.message += " " + result.numLogFiles + " log files"
532                        }
533                                               
534                        if( flash.message ) 
535                                flash.message += "."
536
537                        if( result.numExtraClassificationFiles == 1 ) {
538                                flash.message += result.numExtraClassificationFiles + " additional classification file has been read. ";
539                        } else if( result.numExtraClassificationFiles > 1 ) {
540                                flash.message += result.numExtraClassificationFiles + " additional classification files have been read. ";
541                        }
542
543                        if( result.errors.size() > 0 ) {
544                                flash.error = "However, " + result.errors.size() + " errors occurred during import: "
545                                result.errors.each {
546                                        flash.error += "<br />- " + it
547                                }
548                        }
549                }
550
551                // Determine where to redirect the user to
552                def entityType = session.process[ processId ].entityType;
553                def entityId = session.process[ processId ].entityId;
554                               
555                // Clear session
556                workerService.clearProcess( session, processId );
557               
558                // Redirect user
559                redirect( controller: entityType, action: "show", id: entityId )
560        }
561
562        protected Assay getAssay(def assayId) {
563                // load assay with id specified by param.id
564                def assay
565                try {
566                        assay = Assay.get(assayId as Long)
567                } catch( Exception e ) {
568                        flash.error = "Incorrect id given: " + assayId
569                        return null
570                }
571
572                if (!assay) {
573                        flash.error = "No assay found with id: " + assayId
574                        return null
575                }
576               
577                if (!assay.study.canRead( session.user ) ) {
578                        flash.error = "You don't have the right authorizaton to access assay " + assay.name
579                        return null
580                }
581               
582                return assay
583        }
584       
585        protected Run getRun(def runId) {
586                // load run with id specified by param.id
587                def run
588                try {
589                        run = Run.get(runId as Long)
590                } catch( Exception e ) {
591                        flash.error = "Incorrect id given: " + runId
592                        return null
593                }
594
595                if (!run) {
596                        flash.error = "No run found with id: " + runId
597                        return null
598                }
599
600                return run
601        }
602}
Note: See TracBrowser for help on using the repository browser.