source: trunk/grails-app/services/nl/tno/massSequencing/SampleExcelService.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: 14.2 KB
Line 
1package nl.tno.massSequencing
2
3import org.springframework.web.context.request.RequestContextHolder;
4
5class SampleExcelService {
6        def excelService
7        def fuzzySearchService
8        def gscfService
9       
10        def sessionToken
11       
12    static transactional = false
13
14        // Fields to be edited using excel file and manually
15        def variableFields = [
16                'fwOligo':              'Forward oligo number',
17                'fwMidName':    'Forward mid name',
18                'fwTotalSeq':   'Total forward sequence',
19                'fwMidSeq':             'Forward mid sequence',
20                'fwPrimerSeq':  'Forward primer sequence',
21                'revOligo':     'Reverse oligo number',
22                'revMidName':   'Reverse mid name',
23                'revTotalSeq':  'Total reverse sequence',
24                'revMidSeq':    'Reverse mid sequence',
25                'revPrimerSeq': 'Reverse primer sequence',
26
27        ]
28       
29        def sampleNameName = "Sample name"
30        def runName = "Run"
31        def possibleFields = [sampleNameName, runName] + variableFields.keySet().toList();
32        def possibleFieldNames = [sampleNameName, runName ] + variableFields.values();
33       
34    /**
35     * Download a sample excel file with information about the metagenomics data of the assaySamples (i.e. all assaySample properties)
36     * @param assaySamples      AssaySamples for which the information should be exported
37     * @param includeRun        Whether to include a column with run name or not
38     * @return
39     */
40        def downloadSampleExcel( def assaySamples, boolean includeRun = true ) {
41                def sheetIndex = 0;
42               
43                if( assaySamples == null )
44                        assaySamples = []
45                       
46                def sortedSamples = assaySamples.toList().sort { it.sample.name }
47               
48                // Create an excel sheet
49                def wb = excelService.create();
50
51                def fields = possibleFieldNames
52                if( !includeRun )
53                        fields = fields - runName
54               
55                // Put the headers on the first row
56                excelService.writeHeader( wb, fields, sheetIndex );
57
58                // Adding the next lines
59                ArrayList data = [];
60                sortedSamples.each { assaySample ->
61                        def rowData = [assaySample.sample?.name];
62                        if( includeRun )
63                                rowData << assaySample.run?.name
64                       
65                        variableFields.each { k, v ->
66                                rowData << assaySample[ k ];
67                        }
68                       
69                        data << rowData;
70                }
71                excelService.writeData( wb, data, sheetIndex, 1 );
72
73                // Auto resize columns
74                excelService.autoSizeColumns( wb, sheetIndex, 0..fields.size())
75
76                return wb;
77    }
78       
79        /**
80        * Download a sample excel file with information about the match between sequence files and sample names. This
81        * file is used when uploading sequences
82        * @param assaySamples   AssaySamples for which the information should be exported
83        * @return
84        */
85   def downloadMatchExcel( def assaySamples ) {
86           def sheetIndex = 0;
87           
88           if( assaySamples == null )
89                   assaySamples = []
90                   
91           def sortedSamples = assaySamples.toList().sort { it.sample.name }
92           
93           // Create an excel sheet
94           def wb = excelService.create();
95
96           // Put the headers on the first row
97           excelService.writeHeader( wb, [ "Filename", "MothurSample", "GSCFSample" ], sheetIndex );
98
99           // Adding the next lines
100           ArrayList data = [];
101           sortedSamples.each { assaySample ->
102                   def rowData = [ "", "", assaySample.sample.name ];
103                   
104                   data << rowData;
105           }
106           excelService.writeData( wb, data, sheetIndex, 1 );
107
108           // Auto resize columns
109           excelService.autoSizeColumns( wb, sheetIndex, 0..1 )
110
111           return wb;
112   }
113       
114        /**
115         * Parses a given excel file and tries to match the column names with assaySample properties
116         * @param file 
117         * @return
118         */
119        def parseTagsExcel( File file, boolean includeRun = true ) {
120                def sheetIndex = 0
121                def headerRow = 0
122                def dataStartsAtRow = 1
123                def numExampleRows = 5
124               
125                // Create an excel workbook instance of the file
126                def     workbook = excelService.open( file );
127
128                // Read headers from the first row and 5 of the first lines as example data
129                def headers = excelService.readRow( workbook, sheetIndex, headerRow );
130                def exampleData = excelService.readData( workbook, sheetIndex, dataStartsAtRow, -1, numExampleRows ); // -1 means: determine number of rows yourself
131
132                // Try to guess best matches between the excel file and the column names
133                def bestMatches = [:]
134                def fields = possibleFieldNames
135                if( !includeRun )
136                        fields = fields - runName
137                       
138                // Do matching using fuzzy search. The 0.8 treshold makes sure that no match if chosen if
139                // there is actually no match at all.
140                def matches = fuzzySearchService.mostSimilarUnique( headers, fields, 0.8 );
141               
142                headers.eachWithIndex { header, idx ->
143                        bestMatches[idx] = matches[idx].candidate;
144                }
145               
146                return [headers: headers, exampleData: exampleData, bestMatches: bestMatches, possibleFields: fields]
147        }
148       
149        /**
150         * Updates given assay samples with data from the excel file
151         * @param matchColumns          Indicated which columns from the excel file should go into which field of the assaySample
152         * @param possibleFields        List with possible fields to enter
153         * @param file                          Excel file with the data
154         * @param assaySamples          Assay Samples to be updated
155         * @return
156         */
157        def updateTagsByExcel( def matchColumns, def possibleFields, File file, def assaySamples ) {
158                def sheetIndex = 0
159                def headerRow = 0
160                def dataStartsAtRow = 1
161
162                if( !matchColumns ) {
163                        // Now delete the file, since we don't need it anymore
164                        file?.delete()
165
166                        return [ success: false, message: "No column matches found for excel file. Please try again." ]
167                }
168
169                // Determine column numbers
170                def columns = [:]
171                def dataMatches = false;
172                possibleFieldNames.each { columnName ->
173                        def foundColumn = matchColumns.find { it.value == columnName };
174                       
175                        columns[ columnName ] = ( foundColumn && foundColumn.key.toString().isInteger() ) ? Integer.valueOf( foundColumn.key.toString() ) : -1;
176
177                        if( columnName != sampleNameName && columns[ columnName ] != -1 )
178                                dataMatches = true
179                }
180
181                // A column to match the sample name must be present
182                if( columns[ sampleNameName ] == -1 ) {
183                        return [ success: false, message: "There must be a column present in the excel file that matches the sample name. Please try again." ]
184                }
185
186                // A column with data should also be present
187                if( !dataMatches ) {
188                        return [ success: false, message: "There are no data columns present in the excel file. No samples are updated." ]
189                }
190
191                // Now loop through the excel sheet and update all samples with the specified data
192                if( !file.exists() || !file.canRead() ) {
193                        return [ success: false, message: "Excel file has been removed since previous step. Please try again." ]
194                }
195
196                def workbook = excelService.open( file )
197                ArrayList data = excelService.readData( workbook, sheetIndex, dataStartsAtRow )
198
199                // Check whether the excel file contains any data
200                if( data.size() == 0 ) {
201                        // Now delete the file, since we don't need it anymore
202                        file.delete()
203
204                        return [ success: false, message: "The excel sheet contains no data to import. Please upload another excel file." ]
205                }
206
207                def numSuccesful = 0
208                def failedRows = []
209
210                // walk through all rows and fill the table with records
211                for( def i = 0; i < data.size(); i++ ) {
212                        def rowData = data[ i ];
213
214                        String sampleName = rowData[ columns[ sampleNameName ] ] as String
215
216                        // If no sample name is found, the row is either empty or contains no sample name
217                        if( !sampleName ) { 
218                                failedRows << [ row: rowData, sampleName: "" ];
219                                continue;
220                        }
221                               
222                        // Find assay by sample name. Since sample names are unique within an assay (enforced by GSCF),
223                        // this will always work when only using one assay. When multiple assays are used, this might pose
224                        // a problem
225                        // TODO: Fix problem with multiple assays
226                        AssaySample assaySample = assaySamples.find { 
227                                it.sample.name == sampleName
228                        };
229
230                        // If no assaysample is found, add this row to the failed-row list
231                        if( !assaySample ) {
232                                failedRows << [ row: rowData, sampleName: sampleName ];
233                                continue;
234                        }
235
236                        columns.each {
237                                if( it.value > -1 ) {
238                                        if( it.key == runName ) {
239                                                // If a run name is given, search for that run
240                                                def run;
241                                                if( rowData[ it.value ] ) 
242                                                        run = Run.findByName( rowData[ it.value ].toString() )
243                                               
244                                                // If the run and assay are not coupled, don't add the sample to that
245                                                // run, and don't change the run
246                                                if( run && !run.assays?.contains( assaySample.assay ) ) {
247                                                        // Don't do anything.   
248                                                        log.debug "Trying to add assaySample " + assaySample + " to run " + run + ", but the assay (" + assaySample.assay + ") is not associated with that run."
249                                                } else {
250                                                        if( run ) {
251                                                                assaySample.run = run
252                                                        } else if( assaySample.run ) {
253                                                                assaySample.run.removeFromAssaySamples( assaySample );
254                                                                assaySample.run = null;
255                                                        }
256                                                }
257                                        } else {
258                                                def field = variableFields.find { variableField -> variableField.value == it.key }; 
259                                                if( field ) {
260                                                        assaySample[ field.key ] = rowData[ it.value ];
261                                                }
262                                        }
263                                }
264                        }
265
266                        assaySample.save()
267
268                        numSuccesful++;
269                }
270
271                // Now delete the file, since we don't need it anymore
272                file.delete()
273
274                // Return a message to the user
275                if( numSuccesful == 0 ) {
276                        return [success: false, message: "None of the " + failedRows.size() + " row(s) could be imported, because none of the sample names matched. Have you provided the right excel file?" ]
277                } else {
278                        def message = numSuccesful + " samples have been updated. "
279
280                        if( failedRows.size() > 0 )
281                                message += failedRows.size() + " row(s) could not be imported, because the sample names could not be found in the database."
282
283                        return [success: true, message: message, numSuccesful: numSuccesful, failedRows: failedRows ]
284
285                }
286        }
287       
288        /**
289         * Exports all known data about the samples to an excel file
290         * @param assaySamples  Assaysamples to export information about
291         * @param tags                  Tags associated with the assay samples
292         * @param stream                Outputstream to write the data to       
293         * @return
294         */
295        def exportExcelSampleData( List<AssaySample> assaySamples, def tags, OutputStream stream ) {
296                if( assaySamples == null )
297                        assaySamples = []
298
299                // Gather data from GSCF.
300                def sampleTokens = assaySamples*.sample.unique()*.sampleToken;
301               
302                def gscfData
303                try {
304                        gscfData = gscfService.getSamples( sessionToken, sampleTokens );
305                } catch( Exception e ) {
306                        log.error "Exception occurred while fetching sample data from gscf: " + e.getMessage();
307                        e.printStackTrace()
308                        return false;
309                }
310               
311                // Determine which fields to show from the GSCF data
312                def gscfFields = []
313                def subjectFields = []
314                def eventFields = []
315                def moduleFields = [ "Sample name", "Assay name", "Study name", "Run name", "# sequences", "Artificial tag sequence" ] + variableFields.values();
316                gscfData.each { sample ->
317                        sample.each { key, value ->
318                                if( key == "subjectObject" ) {
319                                        value.each { subjectKey, subjectValue -> 
320                                                if( subjectValue && !value.isNull( subjectKey ) && !subjectFields.contains( subjectKey ) )
321                                                        subjectFields << subjectKey
322                                        }
323                                } else if( key == "eventObject" ) {
324                                        value.each { eventKey, eventValue -> 
325                                                if( eventValue && !value.isNull( eventKey ) && !eventFields.contains( eventKey ) )
326                                                        eventFields << eventKey
327                                        }
328                                } else if( value && !sample.isNull( key ) && !gscfFields.contains( key ) ) {
329                                        gscfFields << key
330                                }
331                        }
332                }
333               
334                // Handle specific fields and names in GSCF
335                def fields = handleSpecificFields( [ "module": moduleFields, "gscf": gscfFields, "subject": subjectFields, "event": eventFields ] );
336
337                // Put the module data in the right format (and sorting the samples by name)
338                def data = []
339                assaySamples.toList().sort { it.sample.name }.each { assaySample ->
340                        // Lookup the tag for this assaySample
341                        def currentTag = tags.find { it.assaySampleId == assaySample.id };
342                       
343                        // First add the module data
344                        def row = [
345                                assaySample.sample.name,
346                                assaySample.assay.name,
347                                assaySample.assay.study.name,
348                                assaySample.run?.name,
349                                assaySample.numSequences(),
350                                currentTag?.tag,
351                        ]
352                       
353                        // Add the variable fields for all assaysamples
354                        variableFields.each { k, v ->
355                                row << assaySample[ k ];
356                        }
357                       
358                        // Afterwards add the gscfData including subject and event data
359                        def gscfRow = gscfData.find { it.sampleToken == assaySample.sample.sampleToken };
360                        if( gscfRow ) {
361                                fields[ "names" ][ "gscf" ].each { field ->
362                                        row << prepare( gscfRow, field );
363                                }
364                                fields[ "names" ][ "subject" ].each { field ->
365                                        row << prepare( gscfRow.optJSONObject( "subjectObject" ), field );
366                                }
367                                fields[ "names" ][ "event" ].each { field ->
368                                        row << prepare( gscfRow.optJSONObject( "eventObject" ), field );
369                                }
370                        }
371                       
372                        // Unfortunately the excel format can only contain max 256 columns
373                        // If more than 256 columns are generated, the columns above 256 are disregarded
374                        if( row.size() > 256 )
375                                row = row[0..255];
376                       
377                        data << row;
378                       
379                }
380               
381                // Create excel file
382                def sheetIndex = 0;
383                       
384                // Create an excel sheet
385                def wb = excelService.create();
386
387                // Put the headers on the first row
388                excelService.writeHeader( wb, fields[ "descriptions" ][ "all" ], sheetIndex );
389                excelService.writeData( wb, data, sheetIndex, 1 );
390
391                // Auto resize columns (# columns = # samples + 1)
392                excelService.autoSizeColumns( wb, sheetIndex, 0..assaySamples?.size())
393
394                // Write the data to the output stream
395                wb.write( stream );
396               
397                return true;
398        }
399       
400        protected String prepare( def object, def fieldName ) {
401                if( !object || object.isNull( fieldName ) )
402                        return "";
403               
404                // If the field is a object, return the 'name' property
405                def obj = object.optJSONObject( fieldName ) 
406                if( obj )
407                        return obj.optString( "name" )
408                else
409                        return object.optString( fieldName );
410        }
411       
412        protected handleSpecificFields( def inputFields ) {
413                def fields = [
414                        "names": [ 
415                                "all": [] 
416                        ],
417                        "descriptions": [
418                                "all": []
419                        ]
420                ]
421               
422                inputFields.each { key, value ->
423                        def names = [];
424                        def descriptions = []
425                        switch( key ) { 
426                                case "gscf":
427                                        value.each {
428                                                if( it != "sampleToken" && it != "name" ) {
429                                                        names << it;
430                                                        if( it == "startTime" )
431                                                                descriptions << "Event start time";
432                                                        else
433                                                                descriptions << it
434                                                }
435                                        }
436                                        break;
437                                case "subject":
438                                        value.each {
439                                                if( it != "name" ) {
440                                                        names << it;
441                                                        descriptions << it
442                                                }
443                                        }
444                                        break;
445                                case "event":
446                                        value.each {
447                                                if( it != "startTime" ) {
448                                                        names << it;
449                                                        descriptions << it
450                                                }
451                                        }
452                                        break;
453                                default:
454                                        names = value; descriptions = value;
455                                        break;
456                        }
457                       
458                        fields[ "names" ][ key ] = names;
459                        fields[ "names" ][ "all" ] += names;
460                        fields[ "descriptions" ][ key ] = descriptions;
461                        fields[ "descriptions" ][ "all" ] += descriptions;
462                }       
463               
464                return fields;
465
466        }
467       
468}
Note: See TracBrowser for help on using the repository browser.