root/trunk/grails-app/services/dbnp/studycapturing/AssayService.groovy @ 2095

Revision 2095, 25.5 KB (checked in by s.h.sikkema@…, 2 years ago)

- Merged assay export and galaxy assay fetch code - now possible to select fetched data in galaxy
- removed unused assay methods and gsps generated by grails
- getUser now returns displayName instead of username in case of shibboleth users

  • Property svn:keywords set to Rev Author Date
Line 
1/**
2 * AssayService Service
3 *
4 * @author  s.h.sikkema@gmail.com
5 * @since       20101216
6 * @package     dbnp.studycapturing
7 *
8 * Revision information:
9 * $Rev$
10 * $Author$
11 * $Date$
12 */
13package dbnp.studycapturing
14
15import org.apache.poi.ss.usermodel.*
16import org.apache.poi.xssf.usermodel.XSSFWorkbook
17import org.apache.poi.hssf.usermodel.HSSFWorkbook
18import org.codehaus.groovy.grails.web.json.JSONObject
19import org.dbnp.gdt.RelTime
20import org.dbnp.gdt.TemplateFieldType
21import java.text.NumberFormat
22import dbnp.authentication.SecUser
23
24class AssayService {
25
26        boolean transactional = false
27        def authenticationService
28        def moduleCommunicationService
29
30        /**
31         * Collects the assay field names per category in a map as well as the
32         * module's measurements.
33         *
34         * @param assay         the assay for which to collect the fields
35         * @param samples       list of samples to retrieve the field names for. If not given, all samples from the assay are used.
36         * @return a map of categories as keys and field names or measurements as
37         *  values
38         */
39        def collectAssayTemplateFields(assay, samples, SecUser remoteUser = null) throws Exception {
40
41                def getUsedTemplateFields = { templateEntities ->
42
43                        // gather all unique and non null template fields that haves values
44                        templateEntities*.giveFields().flatten().unique().findAll{ field ->
45
46                                field && templateEntities.any { it?.fieldExists(field.name) && it.getFieldValue(field.name) != null }
47
48                        }.collect{[name: it.name, comment: it.comment, displayName: it.name + (it.unit ? " ($it.unit)" : '')]}
49                }
50
51                def moduleError = '', moduleMeasurements = []
52
53                try {
54                        moduleMeasurements = requestModuleMeasurementNames(assay, remoteUser)
55                } catch (e) {
56                        moduleError = e.message
57                }
58
59                if( !samples )
60                        samples = assay.samples
61
62                [               'Subject Data' :            getUsedTemplateFields( samples*."parentSubject".unique() ),
63                                        'Sampling Event Data' :     getUsedTemplateFields( samples*."parentEvent".unique() ),
64                                        'Sample Data' :             getUsedTemplateFields( samples ),
65                                        'Event Group' :             [[name: 'name', comment: 'Name of Event Group', displayName: 'name']],
66                                        'Module Measurement Data':  moduleMeasurements,
67                                        'Module Error':             moduleError
68                                ]
69
70        }
71
72        /**
73         * Gathers all assay related data, including measurements from the module,
74         * into 1 hash map containing: Subject Data, Sampling Event Data, Sample
75         * Data, and module specific measurement data.
76         * Data from each of the 4 hash map entries are themselves hash maps
77         * representing a descriptive header (field name) as key and the data as
78         * value.
79         *
80         * @param assay                                 the assay to collect data for
81         * @param fieldMap                              map with categories as keys and fields as values
82         * @param measurementTokens     selection of measurementTokens
83         * @param samples                               list of samples for which the data should be retrieved.
84         *                                                              Defaults to all samples from this assay. Supply [] or
85         *                                                              null to include all samples.
86         * @return                              The assay data structure as described above.
87         */
88        def collectAssayData(assay, fieldMap, measurementTokens, samples, SecUser remoteUser = null) throws Exception {
89
90                def collectFieldValuesForTemplateEntities = { headerFields, templateEntities ->
91
92                        // return a hash map with for each field name all values from the
93                        // template entity list
94                        headerFields.inject([:]) { map, headerField ->
95
96                                map + [(headerField.displayName): templateEntities.collect { entity ->
97
98                                                // default to an empty string
99                                                def val = ''
100
101                                                if (entity) {
102                                                        def field
103                                                        try {
104
105                                                                val = entity.getFieldValue(headerField.name)
106
107                                                                // Convert RelTime fields to human readable strings
108                                                                field = entity.getField(headerField.name)
109                                                                if (field.type == TemplateFieldType.RELTIME)
110                                                                        val = new RelTime( val as long )
111
112                                                        } catch (NoSuchFieldException e) { /* pass */ }
113                                                }
114
115                                                (val instanceof Number) ? val : val.toString()}]
116                        }
117                }
118
119                def getFieldValues = { templateEntities, headerFields, propertyName = '' ->
120
121                        def returnValue
122
123                        // if no property name is given, simply collect the fields and
124                        // values of the template entities themselves
125                        if (propertyName == '') {
126
127                                returnValue = collectFieldValuesForTemplateEntities(headerFields, templateEntities)
128
129                        } else {
130
131                                // if a property name is given, we'll have to do a bit more work
132                                // to ensure efficiency. The reason for this is that for a list
133                                // of template entities, the properties referred to by
134                                // propertyName can include duplicates. For example, for 10
135                                // samples, there may be less than 10 parent subjects. Maybe
136                                // there's only 1 parent subject. We don't want to collect field
137                                // values for this single subject 10 times ...
138                                def fieldValues
139
140                                // we'll get the unique list of properties to make sure we're
141                                // not getting the field values for identical template entity
142                                // properties more then once.
143                                def uniqueProperties = templateEntities*."$propertyName".unique()
144
145                                fieldValues = collectFieldValuesForTemplateEntities(headerFields, uniqueProperties)
146
147                                // prepare a lookup hashMap to be able to map an entities'
148                                // property (e.g. a sample's parent subject) to an index value
149                                // from the field values list
150                                int i = 0
151                                def propertyToFieldValueIndexMap = uniqueProperties.inject([:]) { map, item -> map + [(item):i++]}
152
153                                // prepare the return value so that it has an entry for field
154                                // name. This will be the column name (second header line).
155                                returnValue = headerFields*.displayName.inject([:]) { map, item -> map + [(item):[]] }
156
157                                // finally, fill map the unique field values to the (possibly
158                                // not unique) template entity properties. In our example with
159                                // 1 unique parent subject, this means copying that subject's
160                                // field values to all 10 samples.
161                                templateEntities.each{ te ->
162
163                                        headerFields*.displayName.each{
164
165                                                returnValue[it] << fieldValues[it][propertyToFieldValueIndexMap[te[propertyName]]]
166                                        }
167                                }
168                        }
169                        returnValue
170                }
171
172                // Find samples and sort by name
173                if( !samples )
174                        samples = assay.samples.toList().sort { it.name }
175
176                def eventFieldMap = [:]
177
178                // check whether event group data was requested
179                if (fieldMap['Event Group']) {
180
181                        def names = samples*.parentEventGroup*.name.flatten()
182
183                        // only set name field when there's actual data
184                        if (!names.every {!it}) eventFieldMap['name'] = names
185
186                }
187
188                def moduleError = '', moduleMeasurementData = [:]
189
190                if (measurementTokens) {
191
192                        try {
193                                moduleMeasurementData = requestModuleMeasurements(assay, measurementTokens, samples, remoteUser)
194                        } catch (e) {
195                                moduleMeasurementData = ['error' : ['Module error, module not available or unknown assay'] * samples.size() ]
196                                moduleError =  e.message
197                        }
198
199                }
200
201                [   'Subject Data' :            getFieldValues(samples, fieldMap['Subject Data'], 'parentSubject'),
202                        'Sampling Event Data' :     getFieldValues(samples, fieldMap['Sampling Event Data'], 'parentEvent'),
203                        'Sample Data' :             getFieldValues(samples, fieldMap['Sample Data']),
204                        'Event Group' :             eventFieldMap,
205                        'Module Measurement Data' : moduleMeasurementData,
206                        'Module Error' :            moduleError
207                ]
208        }
209
210        /**
211         * Prepend data from study to the data structure
212         * @param assayData             Column wise data structure of samples
213         * @param assay                 Assay object the data should be selected from
214         * @param numValues             Number of values for this assay
215         * @return                              Extended column wise data structure
216         */
217        def prependStudyData( inputData, Assay assay, numValues ) {
218                if( !assay )
219                        return inputData;
220
221                // Retrieve study data
222                def studyData =[:]
223                assay.parent?.giveFields().each {
224                        def value = assay.parent.getFieldValue( it.name )
225                        if( value )
226                                studyData[ it.name ] = [value] * numValues
227                }
228
229                return [
230                        'Study Data': studyData
231                ] + inputData
232        }
233
234        /**
235         * Prepend data from assay to the data structure
236         * @param assayData             Column wise data structure of samples
237         * @param assay                 Assay object the data should be selected from
238         * @param numValues             Number of values for this assay
239         * @return                              Extended column wise data structure
240         */
241        def prependAssayData( inputData, Assay assay, numValues ) {
242                if( !assay )
243                        return inputData;
244
245                // Retrieve assay data
246                def assayData = [:]
247                assay.giveFields().each {
248                        def value = assay.getFieldValue( it.name )
249                        if( value )
250                                assayData[ it.name ] = [value] * numValues
251                }
252
253                return [
254                        'Assay Data': assayData
255                ] + inputData
256        }
257
258        /**
259         * Retrieves measurement names from the module through a rest call
260         *
261         * @param consumer the url of the module
262         * @param path path of the rest call to the module
263         * @return
264         */
265        def requestModuleMeasurementNames(assay, SecUser remoteUser = null) {
266
267                def moduleUrl = assay.module.url
268
269                def path = moduleUrl + "/rest/getMeasurements/query"
270                def query = "assayToken=${assay.giveUUID()}"
271                def jsonArray
272
273                try {
274                        jsonArray = moduleCommunicationService.callModuleMethod(moduleUrl, path, query, "POST", remoteUser)
275                } catch (e) {
276                        throw new Exception("An error occured while trying to get the measurement tokens from the $assay.module.name. \
277             This means the module containing the measurement data is not available right now. Please try again \
278             later or notify the system administrator if the problem persists. URL: $path?$query.")
279                }
280
281                def result = jsonArray.collect {
282                        if( it == JSONObject.NULL )
283                                return ""
284                        else
285                                return it.toString()
286                }
287
288                return result
289        }
290
291        /**
292         * Retrieves module measurement data through a rest call to the module
293         *
294         * @param assay                         Assay for which the module measurements should be retrieved
295         * @param measurementTokens     List with the names of the fields to be retrieved. Format: [ 'measurementName1', 'measurementName2' ]
296         * @param samples                       Samples to collect measurements for
297         * @return
298         */
299        def requestModuleMeasurements(assay, inputMeasurementTokens, samples, SecUser remoteUser = null) {
300
301                def moduleUrl = assay.module.url
302
303                def tokenString = ''
304
305                inputMeasurementTokens.each{
306                        tokenString+="&measurementToken=${it.encodeAsURL()}"
307                }
308
309                def path = moduleUrl + "/rest/getMeasurementData/query"
310
311                def query = "assayToken=$assay.assayUUID$tokenString"
312
313                def sampleTokens = [], measurementTokens = [], moduleData = []
314
315                try {
316                        (sampleTokens, measurementTokens, moduleData) = moduleCommunicationService.callModuleMethod(moduleUrl, path, query, "POST", remoteUser)
317                } catch (e) {
318                        throw new Exception("An error occured while trying to get the measurement data from the $assay.module.name. \
319             This means the module containing the measurement data is not available right now. Please try again \
320             later or notify the system administrator if the problem persists. URL: $path?$query.")
321                }
322
323                if (!sampleTokens?.size()) return []
324
325                // Convert the three different maps into a map like:
326                //
327                // [ "measurement 1": [ value1, value2, value3 ],
328                //   "measurement 2": [ value4, value5, value6 ] ]
329                //
330                // The returned values should be in the same order as the given samples-list
331                def map = [:]
332                def numSampleTokens = sampleTokens.size();
333
334                measurementTokens.eachWithIndex { measurementToken, measurementIndex ->
335                        def measurements = [];
336                        samples.each { sample ->
337
338                                // Do measurements for this sample exist? If not, a null value is returned
339                                // for this sample. Otherwise, the measurement is looked up in the list with
340                                // measurements, based on the sample token
341                                if( sampleTokens.collect{ it.toString() }.contains( sample.giveUUID() ) ) {
342                                        def tokenIndex = sampleTokens.indexOf( sample.giveUUID() );
343                                        def valueIndex = measurementIndex * numSampleTokens + tokenIndex;
344
345                                        // If the module data is in the wrong format, show an error in the log file
346                                        // and return a null value for this measurement.
347                                        if( valueIndex >= moduleData.size() ) {
348                                                log.error "Module measurements given by module " + assay.module.name + " are not in the right format: " + measurementTokens?.size() + " measurements, " + sampleTokens?.size() + " samples, " + moduleData?.size() + " values"
349                                                measurements << null
350                                        }  else {
351
352                                                def val
353                                                def measurement = moduleData[ valueIndex ]
354
355                                                if          (measurement == JSONObject.NULL)    val = ""
356                                                else if     (measurement instanceof Number)     val = measurement
357                                                else if     (measurement.isDouble())            val = measurement.toDouble()
358                                                else val =   measurement.toString()
359                                                measurements << val
360                                        }
361                                } else {
362                                        measurements << null
363                                }
364                        }
365                        map[ measurementToken.toString() ] = measurements
366                }
367
368                return map;
369        }
370
371        /**
372         * Merges the data from multiple studies into a structure that can be exported to an excel file. The format for each assay is
373         *
374         *      [Category1:
375         *      [Column1: [1,2,3], Column2: [4,5,6]],
376         *   Category2:
377         *      [Column3: [7,8,9], Column4: [10,11,12], Column5: [13,14,15]]]
378         *
379         * Where the category describes the category of data that is presented (e.g. subject, sample etc.) and the column names describe
380         * the fields that are present. Each entry in the lists shows the value for that column for an entity. In this case, 3 entities are described.
381         * Each field should give values for all entities, so the length of all value-lists should be the same.
382         *
383         * Example: If the following input is given (2 assays)
384         *
385         *      [
386         *    [Category1:
387         *      [Column1: [1,2,3], Column2: [4,5,6]],
388         *     Category2:
389         *      [Column3: [7,8,9], Column4: [10,11,12], Column5: [13,14,15]]],
390         *    [Category1:
391         *      [Column1: [16,17], Column6: [18,19]],
392         *     Category3:
393         *      [Column3: [20,21], Column8: [22,23]]]
394         * ]
395         *
396         * the output will be (5 entries for each column, empty values for fields that don't exist in some assays)
397         *
398         *      [
399         *    [Category1:
400         *      [Column1: [1,2,3,16,17], Column2: [4,5,6,,], Column6: [,,,18,19]],
401         *     Category2:
402         *      [Column3: [7,8,9,,], Column4: [10,11,12,,], Column5: [13,14,15,,]],
403         *     Category3:
404         *      [Column3: [,,,20,21], Column8: [,,,22,23]]
405         * ]
406         *
407         *
408         * @param columnWiseAssayData   List with each entry being the column wise data of an assay. The format for each
409         *                                                              entry is described above
410         * @return      Hashmap                         Combined assay data, in the same structure as each input entry. Empty values are given as an empty string.
411         *                                                              So for input entries
412         */
413        def mergeColumnWiseDataOfMultipleStudies(def columnWiseAssayData) {
414                // Compute the number of values that is expected for each assay. This number is
415                // used later on to determine the number of empty fields to add if a field is not present in this
416                // assay
417                def numValues = columnWiseAssayData.collect { assay ->
418                        for( cat in assay ) {
419                                if( cat ) {
420                                        for( field in cat.value ) {
421                                                if( field?.value?.size() > 0 ) {
422                                                        return field.value.size();
423                                                }
424                                        }
425                                }
426                        }
427
428                        return 0;
429                }
430
431                // Merge categories from all assays. Create a list for all categories
432                def categories = columnWiseAssayData*.keySet().toList().flatten().unique();
433                def mergedColumnWiseData = [:]
434                categories.each { category ->
435                        // Only work with this category for all assays
436                        def categoryData = columnWiseAssayData*.getAt( category );
437
438                        // Find the different fields in all assays
439                        def categoryFields = categoryData.findAll{ it }*.keySet().toList().flatten().unique();
440
441                        // Find data for all assays for these fields. If the fields do not exist, return an empty string
442                        def categoryValues = [:]
443                        categoryFields.each { field ->
444                                categoryValues[ field ] = [];
445
446                                // Loop through all assays
447                                categoryData.eachWithIndex { assayValues, idx ->
448                                        if( assayValues && assayValues.containsKey( field ) ) {
449                                                // Append the values if they exist
450                                                categoryValues[ field ] += assayValues[ field ];
451                                        } else {
452                                                // Append empty string for each entity if the field doesn't exist
453                                                categoryValues[ field ] += [""] * numValues[ idx ]
454                                        }
455                                }
456                        }
457
458                        mergedColumnWiseData[ category ] = categoryValues
459                }
460
461                return mergedColumnWiseData;
462        }
463
464        /**
465         * Merges the data from multiple studies into a structure that can be exported to an excel file. The format for each assay is
466         *
467         *      [Category1:
468         *      [Column1: [1,2,3], Column2: [4,5,6]],
469         *   Category2:
470         *      [Column3: [7,8,9], Column4: [10,11,12], Column5: [13,14,15]]]
471         *
472         * Where the category describes the category of data that is presented (e.g. subject, sample etc.) and the column names describe
473         * the fields that are present. Each entry in the lists shows the value for that column for an entity. In this case, 3 entities are described.
474         * Each field should give values for all entities, so the length of all value-lists should be the same.
475         *
476         * Example: If the following input is given (2 assays)
477         *
478         *      [
479         *    [Category1:
480         *      [Column1: [1,2,3], Column2: [4,5,6]],
481         *     Category2:
482         *      [Column3: [7,8,9], Column4: [10,11,12], Column5: [13,14,15]]],
483         *    [Category1:
484         *      [Column1: [16,17], Column6: [18,19]],
485         *     Category3:
486         *      [Column3: [20,21], Column8: [22,23]]]
487         * ]
488         *
489         * the output will be (5 entries for each column, empty values for fields that don't exist in some assays)
490         *
491         *      [
492         *    [Category1:
493         *      [Column1: [1,2,3,16,17], Column2: [4,5,6,,], Column6: [,,,18,19]],
494         *     Category2:
495         *      [Column3: [7,8,9,,], Column4: [10,11,12,,], Column5: [13,14,15,,]],
496         *     Category3:
497         *      [Column3: [,,,20,21], Column8: [,,,22,23]]
498         * ]
499         *
500         *
501         * @param columnWiseAssayData   List with each entry being the column wise data of an assay. The format for each
502         *                                                              entry is described above. The data MUST have a category named 'Sample Data' and in that map a field
503         *                                                              named 'id'. This field is used for matching rows. However, the column is removed, unless
504         *                                                              removeIdColumn is set to false
505         * @param removeIdColumn                If set to true (default), the values for the sample id are removed from the output.
506         * @return      Hashmap                         Combined assay data, in the same structure as each input entry. Empty values are given as an empty string.
507         *                                                              So for input entries
508         */
509        def mergeColumnWiseDataOfMultipleStudiesForASetOfSamples(def columnWiseAssayData, boolean removeIdColumn = true ) {
510                // Merge all assays and studies into one list
511                def mergedData = mergeColumnWiseDataOfMultipleStudies( columnWiseAssayData )
512
513                // A map with keys being the sampleIds, and the values are the indices of that sample in the values list
514                def idMap = [:]
515               
516                // A map with the key being an index in the value list, and the value is the index the values should be copied to
517                def convertMap = [:]
518
519                for( int i = 0; i < mergedData[ "Sample Data" ][ "id" ].size(); i++ ) {
520                        def id = mergedData[ "Sample Data" ][ "id" ][ i ];
521
522                        if( idMap[ id ] == null ) {
523                                // This id occurs for the first time
524                                idMap[ id ] = i;
525                                convertMap[ i ] = i;
526                        } else {
527                                convertMap[ i ] = idMap[ id ];
528                        }
529                }
530               
531                /*
532                 * Example output:
533                 * idMap:      [ 12: 0, 24: 1, 26: 3 ]
534                 * convertMap: [ 0: 0, 1: 1, 2: 0, 3: 3, 4: 3 ]
535                 *   (meaning: rows 0, 1 and 3 should remain, row 2 should be merged with row 0 and row 4 should be merged with row 3)
536                 *   
537                 * The value in the convertMap is always lower than its key. So we sort the convertMap on the keys. That way, we can
538                 * loop through the values and remove the row that has been merged.
539                 */
540               
541                convertMap.sort { a, b -> b.key <=> a.key }.each {
542                        def row = it.key;
543                        def mergeWith = it.value;
544                       
545                        if( row != mergeWith ) {
546                                // Combine the data on row [row] with the data on row [mergeWith]
547                               
548                                mergedData.each {
549                                        def cat = it.key; def fields = it.value;
550                                        fields.each { fieldData ->
551                                                def fieldName = fieldData.key;
552                                                def fieldValues = fieldData.value;
553                                               
554                                                // If one of the fields to merge is empty, use the other one
555                                                // Otherwise the values should be the same (e.g. study, subject, sample data)
556                                                fieldValues[ mergeWith ] = ( fieldValues[ mergeWith ] == null || fieldValues[ mergeWith ] == "" ) ? fieldValues[ row ] : fieldValues[ mergeWith ]
557                                               
558                                                // Remove the row from this list
559                                                fieldValues.remove( row );
560                                        }
561                                }
562                        }
563                }
564               
565                // Remove sample id if required
566                if( removeIdColumn )
567                        mergedData[ "Sample Data" ].remove( "id" );
568               
569                return mergedData
570        }
571
572        /**
573         * Converts column
574         * @param columnData multidimensional map containing column data.
575         * On the top level, the data must be grouped by category. Each key is the
576         * category title and the values are maps representing the columns. Each
577         * column also has a title (its key) and a list of values. Columns must be
578         * equally sized.
579         *
580         * For example, consider the following map:
581         * [Category1:
582         *      [Column1: [1,2,3], Column2: [4,5,6]],
583         *  Category2:
584         *      [Column3: [7,8,9], Column4: [10,11,12], Column5: [13,14,15]]]
585         *
586         * which will be written as:
587         *
588         * | Category1  |           | Category2 |           |           |
589         * | Column1    | Column2   | Column3   | Column4   | Column5   |
590         * | 1          | 4         | 7         | 10        | 13        |
591         * | 2          | 5         | 8         | 11        | 14        |
592         * | 3          | 6         | 9         | 12        | 15        |
593         *
594         * @return row wise data
595         */
596        def convertColumnToRowStructure(columnData) {
597
598                // check if all columns have the dimensionality 2
599                if (columnData.every { it.value.every { it.value instanceof ArrayList } }) {
600
601                        def headers = [[],[]]
602
603                        columnData.each { category ->
604
605                                if (category.value.size()) {
606
607                                        // put category keys into first row separated by null values
608                                        // wherever there are > 1 columns per category
609                                        headers[0] += [category.key] + [null] * (category.value.size() - 1)
610
611                                        // put non-category column headers into 2nd row
612                                        headers[1] += category.value.collect{it.key}
613
614                                }
615
616                        }
617
618                        def d = []
619
620                        // add all column wise data into 'd'
621                        columnData.each { it.value.each { d << it.value } }
622
623                        // transpose d into row wise data and combine with header rows
624                        headers + d.transpose()
625                } else []
626
627        }
628
629        /**
630         * Export column wise data in Excel format to a stream.
631         *
632         * @param columnData Multidimensional map containing column data
633         * @param outputStream Stream to write to
634         * @param useOfficeOpenXML Flag to specify xlsx (standard) or xls output
635         * @return
636         */
637        def exportColumnWiseDataToExcelFile(columnData, outputStream, useOfficeOpenXML = true) {
638
639                // transform data into row based structure for easy writing
640                def rows = convertColumnToRowStructure(columnData)
641
642                if (rows) {
643
644                        exportRowWiseDataToExcelFile(rows, outputStream, useOfficeOpenXML)
645
646                } else {
647
648                        throw new Exception('Wrong column data format.')
649
650                }
651
652        }
653
654        /**
655         * Export row wise data in Excel format to a stream
656         *
657         * @param rowData List of lists containing for each row all cell values
658         * @param outputStream Stream to write to
659         * @param useOfficeOpenXML Flag to specify xlsx (standard) or xls output
660         * @return
661         */
662        def exportRowWiseDataToExcelFile(rowData, outputStream, useOfficeOpenXML = true) {
663                Workbook wb = useOfficeOpenXML ? new XSSFWorkbook() : new HSSFWorkbook()
664                Sheet sheet = wb.createSheet()
665
666                exportRowWiseDataToExcelSheet( rowData, sheet );
667
668                wb.write(outputStream)
669                outputStream.close()
670        }
671
672        /**
673         * Export row wise data in CSV to a stream. All values are surrounded with
674         * double quotes (" ").
675         *
676         * @param rowData List of lists containing for each row all cell values
677         * @param outputStream Stream to write to
678         * @return
679         */
680        def exportRowWiseDataToCSVFile(rowData, outputStream, outputDelimiter = '\t', locale = java.util.Locale.US) {
681
682                def formatter = NumberFormat.getNumberInstance(locale)
683                formatter.setGroupingUsed false // we don't want grouping (thousands) separators
684                formatter.setMaximumFractionDigits(15)
685
686                outputStream << rowData.collect { row ->
687                        row.collect{
688
689                                // omit quotes in case of numeric values and format using chosen locale
690                                if (it instanceof Number) return formatter.format(it)
691
692                                def s = it?.toString() ?: ''
693
694                                def addQuotes = false
695
696                                // escape double quotes with double quotes if they exist and
697                                // enable surround with quotes
698                                if (s.contains('"')) {
699                                        addQuotes = true
700                                        s = s.replaceAll('"','""')
701                                } else {
702                                        // enable surround with quotes in case of comma's
703                                        if (s.contains(',') || s.contains('\n')) addQuotes = true
704                                }
705
706                                addQuotes ? "\"$s\"" : s
707
708                        }.join(outputDelimiter)
709                }.join('\n')
710
711                outputStream.close()
712        }
713
714        /**
715         * Export row wise data for multiple assays in Excel format (separate sheets) to a stream
716         *
717         * @param rowData       List of structures with rowwise data for each assay
718         * @param outputStream Stream to write to
719         * @param useOfficeOpenXML Flag to specify xlsx (standard) or xls output
720         * @return
721         */
722        def exportRowWiseDataForMultipleAssaysToExcelFile(assayData, outputStream, useOfficeOpenXML = true) {
723                Workbook wb = useOfficeOpenXML ? new XSSFWorkbook() : new HSSFWorkbook()
724
725                assayData.each { rowData ->
726                        Sheet sheet = wb.createSheet()
727
728                        exportRowWiseDataToExcelSheet( rowData, sheet );
729                }
730
731                wb.write(outputStream)
732                outputStream.close()
733        }
734
735        /**
736         * Export row wise data in Excel format to a given sheet in an excel workbook
737         *
738         * @param rowData       List of lists containing for each row all cell values
739         * @param sheet         Excel sheet to append the
740         * @return
741         */
742        def exportRowWiseDataToExcelSheet(rowData, Sheet sheet) {
743                // create all rows
744                rowData.size().times { sheet.createRow it }
745
746                sheet.eachWithIndex { Row row, ri ->
747                        if( rowData[ ri ] ) {
748                                // create appropriate number of cells for this row
749                                rowData[ri].size().times { row.createCell it }
750
751                                row.eachWithIndex { Cell cell, ci ->
752
753                                        // Numbers and values of type boolean, String, and Date can be
754                                        // written as is, other types need converting to String
755                                        def value = rowData[ri][ci]
756
757                                        value = (value instanceof Number | value?.class in [boolean.class, String.class, Date.class]) ? value : value?.toString()
758
759                                        // write the value (or an empty String if null) to the cell
760                                        cell.setCellValue(value ?: '')
761
762                                }
763                        }
764                }
765        }
766}
Note: See TracBrowser for help on using the browser.