source: trunk/grails-app/services/nl/tno/metagenomics/SampleExcelService.groovy @ 21

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

Solved issues with fuzzy matching (#23)

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