source: trunk/grails-app/services/nl/tno/massSequencing/SampleExcelService.groovy @ 44

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

Removed mass sample editing (to prevent the edit tags screen opening very slowly). Also added the possibility to add an excel file which matches sequence files to samples (when uploading) (#13). Finally added some 'return false' to onClick events, when dialogs were opened, to prevent the browser from scrolling to the top.

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