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

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

Fixed bug in updating samples and improved rest.getQueryableFieldData to return all fields if no fields are given.

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 { 
225                                it.sample.name == sampleName
226                        };
227
228                        // If no assaysample is found, add this row to the failed-row list
229                        if( !assaySample ) {
230                                failedRows << [ row: rowData, sampleName: sampleName ];
231                                continue;
232                        }
233
234                        columns.each {
235                                if( it.value > -1 ) {
236                                        if( it.key == runName ) {
237                                                assaySample.run = Run.findByName( rowData[ it.value ] );
238                                        } else {
239                                                def field = variableFields.find { variableField -> variableField.value == it.key }; 
240                                                if( field ) {
241                                                        assaySample[ field.key ] = rowData[ it.value ];
242                                                }
243                                        }
244                                }
245                        }
246
247                        assaySample.save()
248
249                        numSuccesful++;
250                }
251
252                // Now delete the file, since we don't need it anymore
253                file.delete()
254
255                // Return a message to the user
256                if( numSuccesful == 0 ) {
257                        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?" ]
258                } else {
259                        def message = numSuccesful + " samples have been updated. "
260
261                        if( failedRows.size() > 0 )
262                                message += failedRows.size() + " row(s) could not be imported, because the sample names could not be found in the database."
263
264                        return [success: true, message: message, numSuccesful: numSuccesful, failedRows: failedRows ]
265
266                }
267        }
268       
269        /**
270         * Exports all known data about the samples to an excel file
271         * @param assaySamples  Assaysamples to export information about
272         * @param tags                  Tags associated with the assay samples
273         * @param stream                Outputstream to write the data to       
274         * @return
275         */
276        def exportExcelSampleData( List<AssaySample> assaySamples, def tags, OutputStream stream ) {
277                if( assaySamples == null )
278                        assaySamples = []
279
280                // Gather data from GSCF.
281                def sampleTokens = assaySamples*.sample.unique()*.sampleToken;
282                def sessionToken = RequestContextHolder.currentRequestAttributes().getSession().sessionToken
283                def gscfData
284                try {
285                        gscfData = gscfService.getSamples( sessionToken, sampleTokens );
286                } catch( Exception e ) {
287                        log.error "Exception occurred while fetching sample data from gscf: " + e.getMessage();
288                        return false;
289                }
290               
291                // Determine which fields to show from the GSCF data
292                def gscfFields = []
293                def subjectFields = []
294                def eventFields = []
295                def moduleFields = [ "Sample name", "Assay name", "Study name", "Run name", "# sequences", "Artificial tag sequence" ] + variableFields.values();
296                gscfData.each { sample ->
297                        sample.each { key, value ->
298                                if( key == "subjectObject" ) {
299                                        value.each { subjectKey, subjectValue -> 
300                                                if( subjectValue && !value.isNull( subjectKey ) && !subjectFields.contains( subjectKey ) )
301                                                        subjectFields << subjectKey
302                                        }
303                                } else if( key == "eventObject" ) {
304                                        value.each { eventKey, eventValue -> 
305                                                if( eventValue && !value.isNull( eventKey ) && !eventFields.contains( eventKey ) )
306                                                        eventFields << eventKey
307                                        }
308                                } else if( value && !sample.isNull( key ) && !gscfFields.contains( key ) ) {
309                                        gscfFields << key
310                                }
311                        }
312                }
313               
314                // Handle specific fields and names in GSCF
315                def fields = handleSpecificFields( [ "module": moduleFields, "gscf": gscfFields, "subject": subjectFields, "event": eventFields ] );
316
317                // Put the module data in the right format (and sorting the samples by name)
318                def data = []
319                assaySamples.toList().sort { it.sample.name }.each { assaySample ->
320                        // Lookup the tag for this assaySample
321                        def currentTag = tags.find { it.assaySampleId == assaySample.id };
322                       
323                        // First add the module data
324                        def row = [
325                                assaySample.sample.name,
326                                assaySample.assay.name,
327                                assaySample.assay.study.name,
328                                assaySample.run?.name,
329                                assaySample.numSequences(),
330                                currentTag?.tag,
331                        ]
332                       
333                        // Add the variable fields for all assaysamples
334                        variableFields.each { k, v ->
335                                row << assaySample[ k ];
336                        }
337                       
338                        // Afterwards add the gscfData including subject and event data
339                        def gscfRow = gscfData.find { it.sampleToken == assaySample.sample.sampleToken };
340                        if( gscfRow ) {
341                                fields[ "names" ][ "gscf" ].each { field ->
342                                        row << prepare( gscfRow, field );
343                                }
344                                fields[ "names" ][ "subject" ].each { field ->
345                                        row << prepare( gscfRow.optJSONObject( "subjectObject" ), field );
346                                }
347                                fields[ "names" ][ "event" ].each { field ->
348                                        row << prepare( gscfRow.optJSONObject( "eventObject" ), field );
349                                }
350                        }
351                       
352                        data << row;
353                       
354                }
355               
356                // Transpose data and create new headers
357                data = data.transpose();
358               
359                // Add field names in front of the data
360                for( int i = 0; i < data.size(); i++ ) {
361                        data[ i ] = [] + fields[ "descriptions" ][ "all" ][ i ] + data[ i ]
362                }
363               
364                // Create excel file
365                def sheetIndex = 0;
366                       
367                // Create an excel sheet
368                def wb = excelService.create();
369
370                // Put the headers on the first row
371                //excelService.writeHeader( wb, data[ 0 ], sheetIndex );
372                excelService.writeData( wb, data, sheetIndex, 0 );
373
374                // Auto resize columns (# columns = # samples + 1)
375                excelService.autoSizeColumns( wb, sheetIndex, 0..assaySamples?.size())
376
377                // Write the data to the output stream
378                wb.write( stream );
379               
380                return true;
381        }
382       
383        protected String prepare( def object, def fieldName ) {
384                if( object.isNull( fieldName ) )
385                        return "";
386               
387                // If the field is a object, return the 'name' property
388                def obj = object.optJSONObject( fieldName ) 
389                if( obj )
390                        return obj.optString( "name" )
391                else
392                        return object.optString( fieldName );
393        }
394       
395        protected handleSpecificFields( def inputFields ) {
396                def fields = [
397                        "names": [ 
398                                "all": [] 
399                        ],
400                        "descriptions": [
401                                "all": []
402                        ]
403                ]
404               
405                inputFields.each { key, value ->
406                        def names = [];
407                        def descriptions = []
408                        switch( key ) { 
409                                case "gscf":
410                                        value.each {
411                                                if( it != "sampleToken" && it != "name" ) {
412                                                        names << it;
413                                                        if( it == "startTime" )
414                                                                descriptions << "Event start time";
415                                                        else
416                                                                descriptions << it
417                                                }
418                                        }
419                                        break;
420                                case "subject":
421                                        value.each {
422                                                if( it != "name" ) {
423                                                        names << it;
424                                                        descriptions << it
425                                                }
426                                        }
427                                        break;
428                                case "event":
429                                        value.each {
430                                                if( it != "startTime" ) {
431                                                        names << it;
432                                                        descriptions << it
433                                                }
434                                        }
435                                        break;
436                                default:
437                                        names = value; descriptions = value;
438                                        break;
439                        }
440                       
441                        fields[ "names" ][ key ] = names;
442                        fields[ "names" ][ "all" ] += names;
443                        fields[ "descriptions" ][ key ] = descriptions;
444                        fields[ "descriptions" ][ "all" ] += descriptions;
445                }       
446               
447                return fields;
448
449        }
450       
451}
Note: See TracBrowser for help on using the repository browser.