source: trunk/grails-app/controllers/nl/tno/metagenomics/AssayController.groovy @ 2

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

Initial import of basic functionality

File size: 18.4 KB
Line 
1package nl.tno.metagenomics
2
3import org.codehaus.groovy.grails.commons.ConfigurationHolder
4
5class AssayController {
6        def synchronizationService
7        def fuzzySearchService
8
9        def fileService
10        def excelService
11        def fastaService
12
13        // Fields to be edited using excel file and manually
14        def sampleNameName = "Sample name"
15        def tagSequenceName = "Tag sequence"
16        def oligoNumberName = "Oligo number"
17        def possibleFields = [sampleNameName, tagSequenceName, oligoNumberName]
18
19        def show = {
20                // load study with id specified by param.id
21                def assay = Assay.get(params.id as Long)
22
23                if (!assay) {
24                        flash.message = "No assay found with id: $params.id"
25                        redirect('action': 'errorPage')
26                        return
27                }
28
29                // Make sure the newest data is available
30                synchronizationService.sessionToken = session.sessionToken
31                synchronizationService.synchronizeAssay( assay );
32
33                // Determine runs not used in this assay
34                def otherRuns = Run.list( sort: "name" ).findAll { !it.assays.contains( assay ) }
35
36                // Send the assay information to the view
37                [assay: assay, editable: assay.study.canWrite( session.user ), otherRuns: otherRuns]
38        }
39
40        def showByToken = {
41                // load study with token specified by param.id
42                def assay = Assay.findByAssayToken(params.id as String)
43
44                if (!assay) {
45                        // Initialize synchronizationService
46                        synchronizationService.sessionToken = session.sessionToken
47                        synchronizationService.user = session.user
48
49                        // If the assay is not found, synchronize studies and try again
50                        synchronizationService.synchronizeStudies();
51
52                        assay = Assay.findByAssayToken(params.id as String)
53
54                        if (!assay) {
55                                // If the assay is not found, synchronize all studies and try again, since we might be out of sync
56                                synchronizationService.eager = true
57                                synchronizationService.synchronizeStudies();
58
59                                assay = Assay.findByAssayToken(params.id as String)
60
61                                // If after synchronization still no assay is found, show an error
62                                if( !assay ) {
63                                        flash.message = "No assay found with token: $params.id"
64                                        redirect('action': 'errorPage')
65                                }
66                        }
67                }
68
69                redirect( action: "show", id: assay.id );
70        }
71
72        /**************************************************************************
73         *
74         * Method for handling data about samples for this assay
75         *
76         *************************************************************************/
77
78        /**
79         * Downloads an excel sheet with data about the assay samples, to enter data in excel
80         */
81        def downloadTagsExcel = {
82                def sheetIndex = 0;
83
84                // load study with id specified by param.id
85                def assay = Assay.get(params.id as Long)
86
87                if (!assay) {
88                        flash.message = "No assay found with id: $params.id"
89                        redirect('action': 'errorPage')
90                        return
91                }
92
93                // Create an excel sheet
94                def wb = excelService.create();
95
96                // Put the headers on the first row
97                excelService.writeHeader( wb, possibleFields, sheetIndex );
98
99                // Adding the next lines
100                def sortedSamples = assay.assaySamples.toList().sort { it.sample.name }
101                ArrayList data = [];
102                sortedSamples.each { assaySample ->
103                        data << [assaySample.sample.name, assaySample.tagSequence, assaySample.oligoNumber ];
104                }
105                excelService.writeData( wb, data, sheetIndex, 1 );
106
107                // Auto resize columns
108                excelService.autoSizeColumns( wb, sheetIndex, 0..2)
109
110                // Make file downloadable
111                log.trace( "Creation for downloading the file " + assay.study.name + "_" + assay.name + "_tags.xls" )
112                excelService.downloadFile( wb, assay.study.name + "_" + assay.name + "_tags.xls", response)
113        }
114
115        /**
116         * Parses an uploaded excel file and shows a form to match columns
117         */
118        def parseTagExcel = {
119                def sheetIndex = 0
120                def headerRow = 0
121                def dataStartsAtRow = 1
122                def numExampleRows = 5
123
124                // load study with id specified by param.id
125                def assay = Assay.get(params.id as Long)
126
127                if (!assay) {
128                        flash.message = "No assay found with id: $params.id"
129                        redirect('action': 'errorPage')
130                        return
131                }
132
133                def filename = params.filename
134
135                // Security check to prevent accessing files in other directories
136                if( !filename || filename.contains( '..' ) ) {
137                        response.status = 500;
138                        render "Invalid filename given";
139                        return;
140                }
141
142                // Check for existence and readability
143                File file = new File( fileService.getUploadDir(), filename)
144
145                if( !file.exists() || !file.canRead() ) {
146                        response.status = 404;
147                        render "The uploaded file doesn't exist or doesn't work as expected.";
148                        return;
149                }
150
151                // Save the filename in session for later use
152                session.filename = filename;
153
154                // Create an excel workbook instance of the file
155                def workbook
156                try {
157                        workbook = excelService.open( file );
158                } catch( Throwable e ) { // Catch a throwable here instead of an exception, since the apache poi stuff gives an Error on failure
159                        // Couldn't create a workbook from this file.
160                        response.status = 400 // Bad request
161                        render "Uploaded file is not a valid excel file."
162                        return
163                }
164
165                // Read headers from the first row and 5 of the first lines as example data
166                def headers = excelService.readRow( workbook, sheetIndex, headerRow );
167                def exampleData = excelService.readData( workbook, sheetIndex, dataStartsAtRow, -1, numExampleRows ); // -1 means: determine number of rows yourself
168
169                // Try to guess best matches between the excel file and the column names
170                def bestMatches = [:]
171
172                headers.eachWithIndex { header, idx ->
173                        // Do matching using fuzzy search. The 0.1 treshold makes sure that no match if chosen if
174                        // there is actually no match at all.
175                        bestMatches[idx] = fuzzySearchService.mostSimilar( header, possibleFields, 0.1 );
176                }
177
178                [assay: assay, headers: headers, exampleData: exampleData, filename: filename, possibleFields: [ "Don't import" ] + possibleFields, bestMatches: bestMatches]
179        }
180
181        /**
182         * Updates the assay samples based on the given excel file and the column matches
183         */
184        def updateTagsByExcel = {
185                def sheetIndex = 0
186                def headerRow = 0
187                def dataStartsAtRow = 1
188
189                // load study with id specified by param.id
190                def assay = Assay.get(params.id as Long)
191
192                if (!assay) {
193                        // Now delete the file, since we don't need it anymore
194                        _deleteUploadedFileFromSession()
195
196                        flash.message = "No assay found with id: $params.id"
197                        redirect('action': 'errorPage')
198                        return
199                }
200
201                if( !session.filename ) {
202                        // Now delete the file, since we don't need it anymore
203                        _deleteUploadedFileFromSession()
204
205                        flash.error = "No excel file found because session timed out. Please try again."
206                        redirect( action: 'show', id: params.id)
207                        return
208                }
209
210                // Determine the match-columns
211                def matchColumns = params[ 'matches'];
212
213                if( !matchColumns ) {
214                        // Now delete the file, since we don't need it anymore
215                        _deleteUploadedFileFromSession()
216
217                        flash.error = "No column matches found for excel file. Please try again."
218                        redirect( action: 'show', id: params.id)
219                        return
220                }
221
222                // Determine column numbers
223                def columns = [:]
224                def dataMatches = false;
225                possibleFields.each { columnName ->
226                        columns[ columnName ] = matchColumns.findIndexOf { it.value == columnName }
227
228                        if( columnName != sampleNameName && columns[ columnName ] != -1 )
229                                dataMatches = true
230                }
231
232                println( "Columns: " + columns)
233
234                // A column to match the sample name must be present
235                if( columns[ sampleNameName ] == -1 ) {
236                        // Now delete the file, since we don't need it anymore
237                        _deleteUploadedFileFromSession()
238
239                        flash.error = "There must be a column present in the excel file that matches the sample name. Please try again."
240                        redirect( action: 'show', id: params.id)
241                        return
242                }
243
244                // A column with data should also be present
245                if( !dataMatches ) {
246                        // Now delete the file, since we don't need it anymore
247                        _deleteUploadedFileFromSession()
248
249                        flash.error = "There are no data columns present in the excel file. No samples are updated."
250                        redirect( action: 'show', id: params.id)
251                        return
252                }
253
254                // Now loop through the excel sheet and update all samples with the specified data
255                File file = new File( fileService.getUploadDir(), session.filename );
256                if( !file.exists() || !file.canRead() ) {
257                        flash.error = "Excel file has been removed since previous step. Please try again."
258                        redirect( action: 'show', id: params.id)
259                        return
260                }
261
262                def workbook = excelService.open( file )
263                ArrayList data = excelService.readData( workbook, sheetIndex, dataStartsAtRow )
264
265                // Check whether the excel file contains any data
266                if( data.size() == 0 ) {
267                        // Now delete the file, since we don't need it anymore
268                        _deleteUploadedFileFromSession()
269
270                        flash.error = "The excel sheet contains no data to import. Please upload another excel file."
271                        redirect( action: 'show', id: params.id)
272
273                        return
274                }
275
276                def numSuccesful = 0
277                def failedRows = []
278
279                // walk through all rows and fill the table with records
280                def assaySamples = assay.assaySamples
281
282                for( def i = 0; i < data.size(); i++ ) {
283                        def rowData = data[ i ];
284
285                        String sampleName = rowData[ columns[ sampleNameName ] ] as String
286
287                        // Find assay by sample name. Since sample names are unique within an assay (enforced by GSCF),
288                        // this will always work.
289                        AssaySample assaySample = assaySamples.find { it.sample.id == Sample.findByName( sampleName )?.id };
290
291                        // If no assaysample is found, add this row to the failed-row list
292                        if( !assaySample ) {
293                                failedRows << [ row: rowData, sampleName: sampleName ];
294                                continue;
295                        }
296
297                        columns.each {
298                                if( it.value > -1 ) {
299                                        switch( it.key ) {
300                                                case tagSequenceName:   assaySample.tagSequence = rowData[ it.value ]; break
301                                                case oligoNumberName:   assaySample.oligoNumber = rowData[ it.value ]; break
302                                        }
303                                }
304                        }
305
306                        assaySample.save()
307
308                        numSuccesful++;
309                }
310
311                // Now delete the file, since we don't need it anymore
312                _deleteUploadedFileFromSession()
313
314                // Return a message to the user
315                if( numSuccesful == 0 ) {
316                        flash.error = "None of the " + failedRows.size() + " row(s) could be imported, because none of the sample names matched. Have you provided the right excel file?"
317                } else {
318                        flash.message = numSuccesful + " samples have been updated. "
319
320                        if( failedRows.size() > 0 )
321                                flash.message += failedRows.size() + " row(s) could not be imported, because the sample names could not be found in the database."
322                }
323                redirect( action: 'show', id: params.id)
324        }
325
326        /**
327         * Update the properties of the assay samples manually
328         */
329        def updateTagsManually = {
330                // load study with id specified by param.id
331                def assay = Assay.get(params.id as Long)
332
333                if (!assay) {
334                        flash.message = "No assay found with id: $params.id"
335                        redirect('action': 'errorPage')
336                        return
337                }
338
339                // Loop through all assay samples and set data
340                def sampleParams = params.assaySample;
341
342                if( sampleParams ) {
343                        assay.assaySamples.each { assaySample ->
344                                def assaySampleParams = sampleParams.get( assaySample.id as String );
345                                if( assaySampleParams ) {
346                                        assaySample.properties = assaySampleParams
347                                        assaySample.save()
348                                }
349                        }
350                }
351
352                flash.message = "Data about samples is saved."
353                redirect( action: 'show', id: params.id )
354        }
355
356        /**************************************************************************
357         *
358         * Methods for handling data about runs for this assay
359         *
360         *************************************************************************/
361
362        /**
363         * Adds existing runs to this assay
364         */
365        def addExistingRuns = {
366                // load study with id specified by param.id
367                def assay = Assay.get(params.id as Long)
368
369                if (!assay) {
370                        flash.message = "No assay found with id: $params.id"
371                        redirect('action': 'errorPage')
372                        return
373                }
374
375                // Add checked runs to this assay
376                def runs = params.runs
377                if( runs instanceof String ) {
378                        runs = [ runs ]
379                }
380
381                def numAdded = 0;
382                runs.each { run_id ->
383                        try {
384                                def run = Run.findById( run_id as Long )
385                                if( run.assays == null || !run.assays.contains( assay ) ) {
386                                        run.addToAssays( assay );
387                                        numAdded++;
388                                }
389                        } catch( Exception e ) {}
390                }
391
392                flash.message = numAdded + " runs are added to this assay."
393                redirect( action: 'show', id: params.id)
394        }
395
396        /**
397         * Adds existing runs to this assay
398         */
399        def removeRun = {
400                // load study with id specified by param.id
401                def assay = Assay.get(params.id as Long)
402
403                if (!assay) {
404                        flash.message = "No assay found with id: $params.id"
405                        redirect('action': 'errorPage')
406                        return
407                }
408
409                if( !params.run_id ) {
410                        flash.message = "No run id given"
411                        redirect(action: 'show', id: params.id)
412                        return
413                }
414
415                def run
416
417                try {
418                        run = Run.findById( params.run_id as Long )
419                } catch( Exception e ) {
420                        throw e
421                        flash.message = "Incorrect run id given: "
422                        redirect(action: 'show', id: params.id)
423                        return
424                }
425
426                if( assay.runs.contains( run ) ) {
427                        assay.removeFromRuns( run );
428                        flash.message = "The run has been removed from this assay."
429                } else {
430                        flash.message = "The given run was not associated with this assay."
431                }
432
433                redirect( action: 'show', id: params.id)
434        }
435
436        /**
437         * Updates existing run
438         */
439        def updateRun = {
440                // load study with id specified by param.id
441                def assay = Assay.get(params.id as Long)
442
443                if (!assay) {
444                        flash.message = "No assay found with id: $params.id"
445                        redirect('action': 'errorPage')
446                        return
447                }
448
449                if( !params.run_id ) {
450                        flash.message = "No run id given"
451                        redirect(action: 'show', id: params.id)
452                        return
453                }
454
455                def run
456
457                try {
458                        run = Run.findById( params.run_id as Long )
459                } catch( Exception e ) {
460                        throw e
461                        flash.message = "Incorrect run id given: "
462                        redirect(action: 'show', id: params.id)
463                        return
464                }
465
466                // Set properties to the run
467                params.parameterFile = params.editParameterFile
468                run.setPropertiesFromForm( params );
469
470                if( run.save() ) {
471                        flash.message = "Run succesfully saved";
472                } else {
473                        flash.error = "Run could not be saved: " + run.getErrors();
474                }
475
476                redirect( action: 'show', id: params.id)
477        }
478
479        def errorPage = {
480                render "An error has occured. $flash.message"
481        }
482
483        /**
484         * Deletes an uploaded file for which the filename is given in the session.
485         * @return
486         */
487        def _deleteUploadedFileFromSession() {
488                if( !session.filename )
489                        return
490
491                // Now delete the file, since we don't need it anymore
492                fileService.delete( session.filename  )
493                session.filename = ''
494        }
495
496        /**************************************************************************
497         *
498         * Methods for handling uploaded sequence and quality files
499         *
500         *************************************************************************/
501
502        /**
503         * Processes uploaded files and tries to combine them with samples
504         */
505        def process = {
506                // load study with id specified by param.id
507                def assay = Assay.get(params.id as Long)
508
509                if (!assay) {
510                        flash.message = "No assay found with id: $params.id"
511                        redirect('action': 'errorPage')
512                        return
513                }
514
515                // Check whether files are given
516                def names = params.sequencefiles
517
518                if( !names ) {
519                        flash.message = "No files uploaded for processing"
520                        redirect('action': 'show', 'id': params.id)
521                        return
522                }
523
524                // If only 1 file is uploaded, it is given as String
525                ArrayList filenames = []
526                if( names instanceof String )
527                        filenames << names
528                else
529                        names.each { filenames << it }
530
531                /* Parses uploaded files, discards files we can not handle
532                 *
533                 * [
534                 *              success: [
535                 *                      [filename: 'abc.fasta', type: FASTA, numSequences: 190]
536                 *                      [filename: 'cde.fasta', type: FASTA, numSequences: 140]
537                 *                      [filename: 'abc.qual', type: QUAL, numSequences: 190, avgQuality: 38]
538                 *                      [filename: 'cde.qual', type: QUAL, numSequences: 140, avgQuality: 29]
539                 *              ],
540                 *              failure: [
541                 *                      [filename: 'testing.xls', message: 'Type not recognized']
542                 *              ]
543                 * ]
544                 */
545                def parsedFiles = fastaService.parseFiles( filenames );
546
547                // Match files with samples in the database
548                def matchedFiles = fastaService.matchFiles( parsedFiles.success, assay.assaySamples );
549
550                // Sort files on filename
551                matchedFiles.sort { a,b -> a.fasta?.originalfilename <=> b.fasta?.originalfilename }
552
553                // Saved file matches in session to use them later on
554                session.processedFiles = [ parsed: parsedFiles, matched: matchedFiles ];
555
556                [assay: assay, parsedFiles: parsedFiles, matchedFiles: matchedFiles, selectedRun: params.selectedRun ]
557        }
558
559        /**
560         * Saves processed files to the database, based on the selections made by the user
561         */
562        def saveProcessedFiles = {
563                // load study with id specified by param.id
564                def assay = Assay.get(params.id as Long)
565
566                if (!assay) {
567                        flash.message = "No assay found with id: $params.id"
568                        redirect('action': 'errorPage')
569                        return
570                }
571
572                // Check whether files are given
573                def files = params.file
574
575                if( !files ) {
576                        flash.message = "No files were selected."
577                        redirect('action': 'show', 'id': params.id)
578                        return
579                }
580
581                File permanentDir = fileService.absolutePath( ConfigurationHolder.config.metagenomics.fileDir )
582                int numSuccesful = 0;
583                def errors = [];
584               
585                // Loop through all files Those are the numeric elements in the 'files' array
586                def digitRE = ~/^\d+$/;
587                files.findAll { it.key.matches( digitRE ) }.each { file ->
588                        def filevalue = file.value;
589                       
590                        // Check if the file is selected
591                        if( filevalue.include == "on" ) {
592                                if( fileService.fileExists( filevalue.fasta ) ) {
593                                        try {
594                                                def permanent = fastaService.savePermanent( filevalue.fasta, filevalue.qual, session.processedFiles );
595                                               
596                                                // Save the data into the database
597                                                SequenceData sd = new SequenceData();
598                                               
599                                                sd.sequenceFile = permanent.fasta
600                                                sd.qualityFile = permanent.qual
601                                                sd.numSequences = permanent.numSequences
602                                                sd.averageQuality = permanent.avgQuality
603                                               
604                                                // Couple the data to the right run and sample
605                                                def run = Run.get( filevalue.run )
606                                                if( run )
607                                                        run.addToSequenceData( sd );
608                                                       
609                                                def sample = AssaySample.get( filevalue.assaySample );
610                                                if( sample )
611                                                        sample.addToSequenceData( sd );
612                                               
613                                                if( !sd.validate() ) {
614                                                        errors << "an error occurred while saving " + filevalue.fasta + ": validation of SequenceData failed.";
615                                                } else {
616                                                        sd.save(flush:true);
617                                                }
618                                               
619                                                numSuccesful++;
620                                        } catch( Exception e ) {
621                                                errors << "an error occurred while saving " + filevalue.fasta + ": " + e.getMessage()
622                                        }
623                                }
624                        } else {
625                                // File doesn't need to be included in the system. Delete it also from disk
626                                fileService.delete( filevalue.fasta );
627                        }
628                }
629
630                // Return a message to the user
631                if( numSuccesful == 0 ) {
632                        flash.error = "None of the files were imported, because "
633                       
634                        if( errors.size() > 0 ) {
635                                errors.each {
636                                        flash.error += "<br />- " + it
637                                }
638                        } else {
639                                flash.error = "none of the files were selected for import."
640                        }
641                } else {
642                        flash.message = numSuccesful + " files have been added to the system. "
643
644                        if( errors.size() > 0 ) {
645                                flash.error += errors.size() + " errors occurred during import: "
646                                errors.each {
647                                        flash.error += "<br />- " + it
648                                }
649                        }
650                }
651               
652                redirect( action: "show", id: params.id )
653        }
654
655}
Note: See TracBrowser for help on using the repository browser.