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

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

Improved user interface and implemented basic export functionality

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