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

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

Externalized configuration; improved assay view (detail views of runs and samples); implemented uploading and parsing of FASTA and QUAL files

File size: 13.7 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
12        // Fields to be edited using excel file and manually
13        def sampleNameName = "Sample name"
14        def tagSequenceName = "Tag sequence"
15        def oligoNumberName = "Oligo number"
16        def possibleFields = [sampleNameName, tagSequenceName, oligoNumberName]
17
18        def show = {
19                // load study with id specified by param.id
20                def assay = Assay.get(params.id as Long)
21
22                if (!assay) {
23                        flash.message = "No assay found with id: $params.id"
24                        redirect('action': 'errorPage')
25                        return
26                }
27
28                // Make sure the newest data is available
29                synchronizationService.sessionToken = session.sessionToken
30                synchronizationService.synchronizeAssay( assay );
31
32                // Determine runs not used in this assay
33                def otherRuns = Run.list( sort: "name" ).findAll { !it.assays.contains( assay ) }
34
35                // Send the assay information to the view
36                [assay: assay, editable: assay.study.canWrite( session.user ), otherRuns: otherRuns]
37        }
38
39        def showByToken = {
40                // load study with token specified by param.id
41                def assay = Assay.findByAssayToken(params.id as String)
42
43                if (!assay) {
44                        // Initialize synchronizationService
45                        synchronizationService.sessionToken = session.sessionToken
46                        synchronizationService.user = session.user
47
48                        // If the assay is not found, synchronize studies and try again
49                        synchronizationService.synchronizeStudies();
50
51                        assay = Assay.findByAssayToken(params.id as String)
52
53                        if (!assay) {
54                                // If the assay is not found, synchronize all studies and try again, since we might be out of sync
55                                synchronizationService.eager = true
56                                synchronizationService.synchronizeStudies();
57
58                                assay = Assay.findByAssayToken(params.id as String)
59
60                                // If after synchronization still no assay is found, show an error
61                                if( !assay ) {
62                                        flash.message = "No assay found with token: $params.id"
63                                        redirect('action': 'errorPage')
64                                }
65                        }
66                }
67
68                redirect( action: "show", id: assay.id );
69        }
70
71        /**************************************************************************
72         *
73         * Method for handling data about samples for this assay
74         *
75         *************************************************************************/
76
77        /**
78         * Downloads an excel sheet with data about the assay samples, to enter data in excel
79         */
80        def downloadTagsExcel = {
81                def sheetIndex = 0;
82
83                // load study with id specified by param.id
84                def assay = Assay.get(params.id as Long)
85
86                if (!assay) {
87                        flash.message = "No assay found with id: $params.id"
88                        redirect('action': 'errorPage')
89                        return
90                }
91
92                // Create an excel sheet
93                def wb = excelService.create();
94
95                // Put the headers on the first row
96                excelService.writeHeader( wb, possibleFields, sheetIndex );
97
98                // Adding the next lines
99                def sortedSamples = assay.assaySamples.toList().sort { it.sample.name }
100                ArrayList data = [];
101                sortedSamples.each { assaySample ->
102                        data << [assaySample.sample.name, assaySample.tagSequence, assaySample.oligoNumber ];
103                }
104                excelService.writeData( wb, data, sheetIndex, 1 );
105
106                // Auto resize columns
107                excelService.autoSizeColumns( wb, sheetIndex, 0..2)
108
109                // Make file downloadable
110                log.trace( "Creation for downloading the file " + assay.study.name + "_" + assay.name + "_tags.xls" )
111                excelService.downloadFile( wb, assay.study.name + "_" + assay.name + "_tags.xls", response)
112        }
113
114        /**
115         * Parses an uploaded excel file and shows a form to match columns
116         */
117        def parseTagExcel = {
118                def sheetIndex = 0
119                def headerRow = 0
120                def dataStartsAtRow = 1
121                def numExampleRows = 5
122
123                // load study with id specified by param.id
124                def assay = Assay.get(params.id as Long)
125
126                if (!assay) {
127                        flash.message = "No assay found with id: $params.id"
128                        redirect('action': 'errorPage')
129                        return
130                }
131
132                def filename = params.filename
133
134                // Security check to prevent accessing files in other directories
135                if( !filename || filename.contains( '..' ) ) {
136                        response.status = 500;
137                        render "Invalid filename given";
138                        return;
139                }
140
141                // Check for existence and readability
142                File file = new File( fileService.getUploadDir(), filename)
143
144                if( !file.exists() || !file.canRead() ) {
145                        response.status = 404;
146                        render "The uploaded file doesn't exist or doesn't work as expected.";
147                        return;
148                }
149
150                // Save the filename in session for later use
151                session.filename = filename;
152
153                // Create an excel workbook instance of the file
154                def workbook
155                try {
156                        workbook = excelService.open( file );
157                } catch( Throwable e ) { // Catch a throwable here instead of an exception, since the apache poi stuff gives an Error on failure
158                        // Couldn't create a workbook from this file.
159                        response.status = 400 // Bad request
160                        render "Uploaded file is not a valid excel file."
161                        return
162                }
163
164                // Read headers from the first row and 5 of the first lines as example data
165                def headers = excelService.readRow( workbook, sheetIndex, headerRow );
166                def exampleData = excelService.readData( workbook, sheetIndex, dataStartsAtRow, -1, numExampleRows ); // -1 means: determine number of rows yourself
167
168                // Try to guess best matches between the excel file and the column names
169                def bestMatches = [:]
170
171                headers.eachWithIndex { header, idx ->
172                        // Do matching using fuzzy search. The 0.1 treshold makes sure that no match if chosen if
173                        // there is actually no match at all.
174                        bestMatches[idx] = fuzzySearchService.mostSimilar( header, possibleFields, 0.1 );
175                }
176
177                [assay: assay, headers: headers, exampleData: exampleData, filename: filename, possibleFields: [ "Don't import" ] + possibleFields, bestMatches: bestMatches]
178        }
179
180        /**
181         * Updates the assay samples based on the given excel file and the column matches
182         */
183        def updateTagsByExcel = {
184                def sheetIndex = 0
185                def headerRow = 0
186                def dataStartsAtRow = 1
187
188                // load study with id specified by param.id
189                def assay = Assay.get(params.id as Long)
190
191                if (!assay) {
192                        // Now delete the file, since we don't need it anymore
193                        _deleteUploadedFileFromSession()
194
195                        flash.message = "No assay found with id: $params.id"
196                        redirect('action': 'errorPage')
197                        return
198                }
199
200                if( !session.filename ) {
201                        // Now delete the file, since we don't need it anymore
202                        _deleteUploadedFileFromSession()
203
204                        flash.error = "No excel file found because session timed out. Please try again."
205                        redirect( action: 'show', id: params.id)
206                        return
207                }
208
209                // Determine the match-columns
210                def matchColumns = params[ 'matches'];
211
212                if( !matchColumns ) {
213                        // Now delete the file, since we don't need it anymore
214                        _deleteUploadedFileFromSession()
215
216                        flash.error = "No column matches found for excel file. Please try again."
217                        redirect( action: 'show', id: params.id)
218                        return
219                }
220
221                // Determine column numbers
222                def columns = [:]
223                def dataMatches = false;
224                possibleFields.each { columnName ->
225                        columns[ columnName ] = matchColumns.findIndexOf { it.value == columnName }
226
227                        if( columnName != sampleNameName && columns[ columnName ] != -1 )
228                                dataMatches = true
229                }
230
231                println( "Columns: " + columns)
232
233                // A column to match the sample name must be present
234                if( columns[ sampleNameName ] == -1 ) {
235                        // Now delete the file, since we don't need it anymore
236                        _deleteUploadedFileFromSession()
237
238                        flash.error = "There must be a column present in the excel file that matches the sample name. Please try again."
239                        redirect( action: 'show', id: params.id)
240                        return
241                }
242
243                // A column with data should also be present
244                if( !dataMatches ) {
245                        // Now delete the file, since we don't need it anymore
246                        _deleteUploadedFileFromSession()
247
248                        flash.error = "There are no data columns present in the excel file. No samples are updated."
249                        redirect( action: 'show', id: params.id)
250                        return
251                }
252
253                // Now loop through the excel sheet and update all samples with the specified data
254                File file = new File( fileService.getUploadDir(), session.filename );
255                if( !file.exists() || !file.canRead() ) {
256                        flash.error = "Excel file has been removed since previous step. Please try again."
257                        redirect( action: 'show', id: params.id)
258                        return
259                }
260
261                def workbook = excelService.open( file )
262                ArrayList data = excelService.readData( workbook, sheetIndex, dataStartsAtRow )
263
264                // Check whether the excel file contains any data
265                if( data.size() == 0 ) {
266                        // Now delete the file, since we don't need it anymore
267                        _deleteUploadedFileFromSession()
268
269                        flash.error = "The excel sheet contains no data to import. Please upload another excel file."
270                        redirect( action: 'show', id: params.id)
271
272                        return
273                }
274
275                def numSuccesful = 0
276                def failedRows = []
277
278                // walk through all rows and fill the table with records
279                def assaySamples = assay.assaySamples
280
281                for( def i = 0; i < data.size(); i++ ) {
282                        def rowData = data[ i ];
283
284                        String sampleName = rowData[ columns[ sampleNameName ] ] as String
285
286                        // Find assay by sample name. Since sample names are unique within an assay (enforced by GSCF),
287                        // this will always work.
288                        AssaySample assaySample = assaySamples.find { it.sample.id == Sample.findByName( sampleName )?.id };
289
290                        // If no assaysample is found, add this row to the failed-row list
291                        if( !assaySample ) {
292                                failedRows << [ row: rowData, sampleName: sampleName ];
293                                continue;
294                        }
295
296                        columns.each {
297                                if( it.value > -1 ) {
298                                        switch( it.key ) {
299                                                case tagSequenceName:   assaySample.tagSequence = rowData[ it.value ]; break
300                                                case oligoNumberName:   assaySample.oligoNumber = rowData[ it.value ]; break
301                                        }
302                                }
303                        }
304
305                        assaySample.save()
306
307                        numSuccesful++;
308                }
309
310                // Now delete the file, since we don't need it anymore
311                _deleteUploadedFileFromSession()
312
313                // Return a message to the user
314                if( numSuccesful == 0 ) {
315                        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?"
316                } else {
317                        flash.message = numSuccesful + " samples have been updated. "
318
319                        if( failedRows.size() > 0 )
320                                flash.message += failedRows.size() + " row(s) could not be imported, because the sample names could not be found in the database."
321                }
322                redirect( action: 'show', id: params.id)
323        }
324
325        /**
326         * Update the properties of the assay samples manually
327         */
328        def updateTagsManually = {
329                // load study with id specified by param.id
330                def assay = Assay.get(params.id as Long)
331
332                if (!assay) {
333                        flash.message = "No assay found with id: $params.id"
334                        redirect('action': 'errorPage')
335                        return
336                }
337
338                // Loop through all assay samples and set data
339                def sampleParams = params.assaySample;
340
341                if( sampleParams ) {
342                        assay.assaySamples.each { assaySample ->
343                                def assaySampleParams = sampleParams.get( assaySample.id as String );
344                                if( assaySampleParams ) {
345                                        assaySample.properties = assaySampleParams
346                                        assaySample.save()
347                                }
348                        }
349                }
350
351                flash.message = "Data about samples is saved."
352                redirect( action: 'show', id: params.id )
353        }
354
355        /**************************************************************************
356         *
357         * Methods for handling data about runs for this assay
358         *
359         *************************************************************************/
360
361        /**
362         * Adds existing runs to this assay
363         */
364        def addExistingRuns = {
365                // load study with id specified by param.id
366                def assay = Assay.get(params.id as Long)
367
368                if (!assay) {
369                        flash.message = "No assay found with id: $params.id"
370                        redirect('action': 'errorPage')
371                        return
372                }
373
374                // Add checked runs to this assay
375                def runs = params.runs
376                if( runs instanceof String ) {
377                        runs = [ runs ]
378                }
379
380                def numAdded = 0;
381                runs.each { run_id ->
382                        try {
383                                def run = Run.findById( run_id as Long )
384                                if( run.assays == null || !run.assays.contains( assay ) ) {
385                                        run.addToAssays( assay );
386                                        numAdded++;
387                                }
388                        } catch( Exception e ) {}
389                }
390
391                flash.message = numAdded + " runs are added to this assay."
392                redirect( action: 'show', id: params.id)
393        }
394
395        /**
396         * Adds existing runs to this assay
397         */
398        def removeRun = {
399                // load study with id specified by param.id
400                def assay = Assay.get(params.id as Long)
401
402                if (!assay) {
403                        flash.message = "No assay found with id: $params.id"
404                        redirect('action': 'errorPage')
405                        return
406                }
407
408                if( !params.run_id ) {
409                        flash.message = "No run id given"
410                        redirect(action: 'show', id: params.id)
411                        return
412                }
413
414                def run
415
416                try {
417                        run = Run.findById( params.run_id as Long )
418                } catch( Exception e ) {
419                        throw e
420                        flash.message = "Incorrect run id given: "
421                        redirect(action: 'show', id: params.id)
422                        return
423                }
424
425                if( assay.runs.contains( run ) ) {
426                        assay.removeFromRuns( run );
427                        flash.message = "The run has been removed from this assay."
428                } else {
429                        flash.message = "The given run was not associated with this assay."
430                }
431
432                redirect( action: 'show', id: params.id)
433        }
434
435        /**
436         * Updates existing run
437         */
438        def updateRun = {
439                // load study with id specified by param.id
440                def assay = Assay.get(params.id as Long)
441
442                if (!assay) {
443                        flash.message = "No assay found with id: $params.id"
444                        redirect('action': 'errorPage')
445                        return
446                }
447
448                if( !params.run_id ) {
449                        flash.message = "No run id given"
450                        redirect(action: 'show', id: params.id)
451                        return
452                }
453
454                def run
455
456                try {
457                        run = Run.findById( params.run_id as Long )
458                } catch( Exception e ) {
459                        throw e
460                        flash.message = "Incorrect run id given: "
461                        redirect(action: 'show', id: params.id)
462                        return
463                }
464
465                // Set properties to the run
466                params.parameterFile = params.editParameterFile
467                run.setPropertiesFromForm( params );
468
469                if( run.save() ) {
470                        flash.message = "Run succesfully saved";
471                } else {
472                        flash.error = "Run could not be saved: " + run.getErrors();
473                }
474
475                redirect( action: 'show', id: params.id)
476        }
477
478        def errorPage = {
479                render "An error has occured. $flash.message"
480        }
481
482        /**
483         * Deletes an uploaded file for which the filename is given in the session.
484         * @return
485         */
486        def _deleteUploadedFileFromSession() {
487                if( !session.filename )
488                        return
489
490                // Now delete the file, since we don't need it anymore
491                fileService.delete( session.filename  )
492                session.filename = ''
493        }
494
495}
Note: See TracBrowser for help on using the repository browser.