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

Last change on this file since 70 was 70, checked in by robert@…, 8 years ago
  • Installed templates (in order to extend session lifetime to 2 hours)
  • Implemented background worker to do work outside the HTTP request
File size: 18.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                   // Tell the frontend we are finished
151                   httpSession.progress[ processId ].finished = true;
152           }
153               
154                redirect( url: url );
155        }
156       
157        /**
158         * Show result of processing uploaded files (step 1)
159         */
160        def parseUploadResult = {
161                def processId = params.processId;
162                // load study with id specified by param.id
163                def entity
164               
165                switch( session.process[ processId ].entityType ) {
166                        case "run":
167                                entity = getRun( session.process[ processId ].entityId )
168                                break;
169                        case "assay":
170                                entity = getAssay( session.process[ processId ].entityId )
171                                break;
172                        default:
173                                response.setStatus( 404, "No entity found" );
174                                render "";
175                                return;
176                }
177               
178                def assaySamples = entity.assaySamples.findAll { it.assay.study.canWrite( session.user ) };
179               
180                if (!entity) {
181                        response.setStatus( 404, flash.error )
182                        render "";
183                        return
184                }
185               
186                if( !session.process[ processId ].processedFiles ) {
187                        flash.error = "Processing of files failed. Maybe the session timed out."
188                        redirect( controller: params.entityType, action: 'show', 'id': params.id)
189                        return
190                }
191               
192                // Find matching sequenceData objects for taxonomyfiles that have not been matched
193                def notMatchedFiles = session.process[ processId ].processedFiles.notMatched;
194                def extraClassifications = notMatchedFiles.findAll { it.type == "taxonomy" }
195                extraClassifications.collect {
196                        // Find all sequence files that have the correct number of sequences and are in the list of assaySamples
197                        it[ 'feasibleSequenceData' ] = SequenceData.findAllByNumSequences( it.numLines ).findAll { assaySamples.contains( it.sample ) }
198                        return it
199                }
200               
201                [       entityType: session.process[ processId ].entityType, processId: processId, entity: entity, 
202                        parsedFiles: session.process[ processId ].processedFiles.parsed, 
203                        matchedFiles: session.process[ processId ].processedFiles.matched, 
204                        remainingClassificationFiles: extraClassifications,
205                        selectedRun: params.selectedRun ]
206        }
207
208        /**
209         * Returns from the upload wizard without saving the data. The uploaded files are removed
210         */
211        def returnWithoutSaving = {
212                def processId = params.processId;
213                def entityType = session.process[ processId ].entityType;
214                def entityId = session.process[ processId ].entityId;
215               
216                // Delete all uploaded files from disk
217                session.process[ processId ]?.processedFiles?.parsed?.success?.each {
218                        fileService.delete( it.filename );
219                }
220               
221                // Clear process from session
222                workerService.clearProcess( session, processId );
223
224                // Redirect to the correct controller           
225                switch( entityType ) {
226                        case "run":
227                        case "assay":
228                                redirect( controller: entityType, action: "show", id: entityId );
229                                return;
230                        default:
231                                response.setStatus( 404, "No entity found" );
232                                render "";
233                                return;
234                }
235               
236               
237        }
238       
239        /**
240         * Shows a screen with the progress of saving matched files
241         */
242        def saveMatchedFiles = {
243                def processId = params.processId
244
245                def entityType = session.process[ processId ].entityType
246                def entityId = session.process[ processId ].entityId
247               
248                session.process[ processId ].matchedFiles = params.file
249                session.process[ processId ].matchedRemainingClassification = params.remainingClassification
250               
251                // Check for total size of the classification files in order to be able
252                // to show a progress bar. The handling of classification files is orders
253                // of magnitude bigger than the rest, so we only show progress of those files
254                long filesize = 0;
255
256                // Loop through all files. Those are the numeric elements in the 'files' array
257                def digitRE = ~/^\d+$/;
258                params.file.findAll { it.key.matches( digitRE ) }.each { file ->
259                        def filevalue = file.value;
260                       
261                        // Check if the file is selected
262                        if( filevalue.include == "on" ) {
263                                if( fileService.fileExists( filevalue.fasta ) ) {
264                                        // Also save classification data for this file, if it is present
265                                        if( filevalue.classification ) {
266                                                filesize += fileService.get( filevalue.classification )?.size()
267                                        }
268                                }
269                        }
270                }
271                params.remainingClassification.findAll { it.key.matches( digitRE ) }.each { file ->
272                        def filevalue = file.value;
273                       
274                        // Check if the file is selected
275                        if( filevalue.include == "on" ) {
276                                if( fileService.fileExists( filevalue.filename ) ) {
277                                        // Also save classification data for this file, if it is present
278                                        filesize += fileService.get( filevalue.filename )?.size()
279                                }
280                        }
281                }
282
283                // Clear old process, but save useful data
284                def processInfo = session.process[ processId ]
285                workerService.clearProcess( session, processId );
286               
287                // Create a new unique process identifier
288                processId = workerService.initProcess( session, "Store sequence data and classification", 2, filesize );
289               
290                session.progress[ processId ].stepNum = 2;
291                session.process[ processId ] = processInfo;
292               
293                // Retrieve worker URL
294                def finishUrl = createLink( controller: "import", action: 'saveMatchedResult', params: [ processId: processId ] ).toString();
295                def returnUrl = createLink( controller: entityType, action: "show", entityId ).toString();
296               
297                def url = workerService.startProcess( session, processId, finishUrl, returnUrl )
298               
299                //
300                // Initiate work
301                //
302                // Check whether files are given
303                def files = session.process[ processId ].matchedFiles
304                def remainingClassification = session.process[ processId ].matchedRemainingClassification;
305               
306                if( !files && !remainingClassification ) {
307                        flash.message = "No files were selected for import."
308                        redirect( controller: session.process[ processId ].entityType, action: 'show', 'id': session.process[ processId ].entityId)
309                        return
310                }
311
312                File permanentDir = fileService.absolutePath( ConfigurationHolder.config.massSequencing.fileDir )
313               
314                // This closure enables keeping track of the progress
315                def httpSession = session;
316                def onProgress = { progress ->
317                        // Update progress
318                        httpSession.progress[ processId ].stepProgress += progress;
319                }
320               
321                // Run the computations asynchronously, since it takes a lot of time
322                runAsync {
323                        // Loop through all FASTA files. Those are the numeric elements in the 'files' array
324                        def fastaReturn = saveMatchedFastaFiles( files, httpSession.process[ processId ]?.processedFiles, onProgress );
325                        def classificationReturn = saveRemainingClassificationFiles( remainingClassification, onProgress );
326                       
327                        // Update classification (summary) for updated samples
328                        def samplesClassified = [] + fastaReturn.samplesClassified + classificationReturn.samplesClassified;
329                        classificationService.updateClassificationForAssaySamples( samplesClassified.findAll { it }.unique() )
330                       
331                        def returnStructure = [
332                                numSequenceFiles: fastaReturn.numSequenceFiles,
333                                numQualFiles: fastaReturn.numQualFiles,
334                                numClassificationFiles: fastaReturn.numClassificationFiles,
335                                numExtraClassificationFiles: classificationReturn.numExtraClassifications,
336                                numTotal: fastaReturn.numSequenceFiles + classificationReturn.numExtraClassifications,
337                                errors: [] + fastaReturn.errors + classificationReturn.errors
338                        ]
339                       
340                        // Return all files that have not been moved
341                        httpSession.process[ processId ]?.processedFiles?.parsed?.success?.each {
342                                fileService.delete( it.filename );
343                        }
344                       
345                        httpSession.process[ processId ].result = returnStructure;
346                       
347                        // Tell the frontend we are finished
348                        httpSession.progress[ processId ].finished = true;
349       
350                }
351               
352                redirect( url: url );
353        }
354       
355        def saveMatchedFastaFiles( def files, processedFiles, Closure onProgress ) {
356                int numSuccesful = 0;
357                int numQualFiles = 0;
358                int numClassificationFiles = 0;
359                def samplesClassified = [];
360                def errors = [];
361
362                def digitRE = ~/^\d+$/;
363                files.findAll { it.key.matches( digitRE ) }.each { file ->
364                        def filevalue = file.value;
365                       
366                        // Check if the file is selected
367                        if( filevalue.include == "on" ) {
368                                if( fileService.fileExists( filevalue.fasta ) ) {
369                                        try {
370                                                def permanent = fastaService.savePermanent( filevalue.fasta, filevalue.qual, processedFiles );
371                                               
372                                                // Save the data into the database
373                                                SequenceData sd = new SequenceData();
374                                               
375                                                sd.sequenceFile = permanent.fasta
376                                                sd.qualityFile = permanent.qual
377                                                sd.numSequences = permanent.numSequences
378                                                sd.averageQuality = permanent.avgQuality
379                                                       
380                                                def sample = AssaySample.get( filevalue.assaySample );
381                                                if( sample ) {
382                                                        sample.addToSequenceData( sd );
383                                                       
384                                                        AssaySample.recalculateNumSequences( sample );
385                                                }
386                                               
387                                                if( !sd.validate() ) {
388                                                        errors << "an error occurred while saving " + filevalue.fasta + ": validation of SequenceData failed.";
389                                                } else {
390                                                        sd.save(flush:true);
391                                                       
392                                                        // Also save classification data for this file, if it is present
393                                                        if( filevalue.classification ) {
394                                                                classificationService.storeClassification( filevalue.classification, sd, onProgress )
395                                                                samplesClassified << sample
396                                                               
397                                                                numClassificationFiles++;
398                                                        }
399                                                       
400                                                        if( sd.qualityFile )
401                                                                numQualFiles++;
402                                                       
403                                                        numSuccesful++;
404                                                }
405                                        } catch( Exception e ) {
406                                                e.printStackTrace();
407                                                errors << "an error occurred while saving " + filevalue.fasta + ": " + e.getMessage()
408                                        }
409                                }
410                        } else {
411                                // File doesn't need to be included in the system. Delete it also from disk
412                                fileService.delete( filevalue.fasta );
413                        }
414                }
415               
416                return [ numSequenceFiles: numSuccesful, numQualFiles: numQualFiles, numClassificationFiles: numClassificationFiles, errors: errors, samplesClassified: samplesClassified.unique() ]
417        }
418       
419        def saveRemainingClassificationFiles( def files, Closure onProgress ) {
420                def digitRE = ~/^\d+$/;
421                def errors = [];
422                def samplesClassified = [];
423                def numSuccesful = 0;
424               
425                files.findAll { it.key.matches( digitRE ) }.each { file ->
426                        def filevalue = file.value;
427                       
428                        // Check if the file is selected
429                        if( filevalue.include == "on" ) {
430                                if( fileService.fileExists( filevalue.filename ) ) {
431                                        def sequenceDataId = filevalue.sequenceData;
432                                        try {
433                                                if( sequenceDataId.toString().isLong() ) {
434                                                        // Retrieve sequenceData and sample now, because the session will be cleared during import
435                                                        def sequenceData = SequenceData.get( sequenceDataId.toString().toLong() );
436                                                        def sample = sequenceData.sample;
437                                                       
438                                                        if( sequenceData ) {
439                                                                classificationService.removeClassificationForSequenceData( sequenceData );
440                                                                classificationService.storeClassification( filevalue.filename, sequenceData, onProgress )
441                                                                samplesClassified << sample;
442                                                        }
443       
444                                                        numSuccesful++;
445                                                } else {
446                                                        errors << "a wrong ID is entered for classification file " + filevalue.filename;
447                                                }
448                                        } catch( Exception e ) {
449                                                e.printStackTrace();
450                                                errors << "an error occurred while saving " + filevalue.filename + ": " + e.getMessage()
451                                        }
452                                }
453                        }
454                       
455                        // File doesn't need to be included in the system. Delete it from disk.
456                        fileService.delete( filevalue.filename );
457                }
458               
459                return [ numExtraClassifications: numSuccesful, errors: errors, samplesClassified: samplesClassified.unique()  ]
460               
461        }
462
463        /**
464         * Redirects the user back to the start screen with a message about how things went
465         */
466        def saveMatchedResult = {
467                def processId = params.processId
468               
469                def result = session.process[ processId ].result 
470
471                // Return a message to the user
472                if( result.numTotal == 0 ) {
473                       
474                        if( result.errors.size() > 0 ) {
475                                flash.error = "None of the files were imported, because "
476                                result.errors.each {
477                                        flash.error += "<br />- " + it
478                                }
479                        } else {
480                                flash.message = "None of the files were imported, because none of the files were selected for import."
481                        }
482                } else {
483                        flash.message = ""     
484                        if( result.numSequenceFiles == 1 ) {
485                                flash.message += result.numSequenceFiles + " sequence file has been added to the system"
486                        } else if( result.numSequenceFiles > 1 ) {
487                                flash.message += result.numSequenceFiles + " sequence files have been added to the system"
488                        }
489                       
490                        if( result.numQualFiles > 0 || result.numClassificationFiles > 0 ) {
491                                flash.message += ", with";
492                        }
493                       
494                        if( result.numQualFiles == 1 ) {
495                                flash.message += " 1 quality file"
496                        } else if( result.numQualFiles > 1 ) {
497                                flash.message += " " + result.numQualFiles + " quality files"
498                        }
499                       
500                        if( result.numQualFiles > 0 && result.numClassificationFiles > 0 ) {
501                                flash.message += " and";
502                        }
503                       
504                        if( result.numClassificationFiles == 1 ) {
505                                flash.message += " 1 classification file"
506                        } else if( result.numClassificationFiles > 1 ) {
507                                flash.message += " " + result.numClassificationFiles + " classification files"
508                        }
509                       
510                        if( flash.message ) 
511                                flash.message += "."
512
513                        if( result.numExtraClassificationFiles == 1 ) {
514                                flash.message += result.numExtraClassificationFiles + " additional classification file has been read. ";
515                        } else if( result.numExtraClassificationFiles > 1 ) {
516                                flash.message += result.numExtraClassificationFiles + " additional classification files have been read. ";
517                        }
518
519                        if( result.errors.size() > 0 ) {
520                                flash.error = "However, " + result.errors.size() + " errors occurred during import: "
521                                result.errors.each {
522                                        flash.error += "<br />- " + it
523                                }
524                        }
525                }
526
527                // Determine where to redirect the user to
528                def entityType = session.process[ processId ].entityType;
529                def entityId = session.process[ processId ].entityId;
530                               
531                // Clear session
532                workerService.clearProcess( session, processId );
533               
534                // Redirect user
535                redirect( controller: entityType, action: "show", id: entityId )
536        }
537
538        protected Assay getAssay(def assayId) {
539                // load assay with id specified by param.id
540                def assay
541                try {
542                        assay = Assay.get(assayId as Long)
543                } catch( Exception e ) {
544                        flash.error = "Incorrect id given: " + assayId
545                        return null
546                }
547
548                if (!assay) {
549                        flash.error = "No assay found with id: " + assayId
550                        return null
551                }
552               
553                if (!assay.study.canRead( session.user ) ) {
554                        flash.error = "You don't have the right authorizaton to access assay " + assay.name
555                        return null
556                }
557               
558                return assay
559        }
560       
561        protected Run getRun(def runId) {
562                // load run with id specified by param.id
563                def run
564                try {
565                        run = Run.get(runId as Long)
566                } catch( Exception e ) {
567                        flash.error = "Incorrect id given: " + runId
568                        return null
569                }
570
571                if (!run) {
572                        flash.error = "No run found with id: " + runId
573                        return null
574                }
575
576                return run
577        }
578}
Note: See TracBrowser for help on using the repository browser.