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

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

Renamed module to massSequencing

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