source: trunk/grails-app/services/dbnp/studycapturing/AssayService.groovy @ 1969

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

Added the possibility to export assay data for samples after searching. This also works for samples with multiple assays.

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