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

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

Implemented export possibility and bugfixes

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