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

Last change on this file since 2156 was 2156, checked in by m.s.vanvliet@…, 10 years ago

Added support for exporting meta-data from a module.
When meta-data is present it will restructure the export format to include the measurement meta-data (including the properties)

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