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

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

Improved querying (#40)

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