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

Revision 1559, 12.0 KB (checked in by s.h.sikkema@…, 3 years ago)

Assay export functionality

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
18
19class AssayService {
20
21    boolean transactional = true
22    def authenticationService
23    def moduleCommunicationService
24
25    /**
26     * Collects the assay field names per category in a map as well as the
27     * module's measurements.
28     *
29     * @param assay the assay for which to collect the fields
30     * @return a map of categories as keys and field names or measurements as
31     *  values
32     */
33    def collectAssayTemplateFields(assay) throws Exception {
34
35        def getUsedTemplateFieldNames = { templateEntities ->
36
37            // gather all unique and non null template fields that haves values
38            templateEntities*.giveFields().flatten().unique().findAll{ field ->
39
40                field && templateEntities.any { it.fieldExists(field.name) && it.getFieldValue(field.name) }
41
42            }*.name
43
44        }
45
46        // check whether module is reachable
47        if (!moduleCommunicationService.isModuleReachable(assay.module.url)) {
48
49            throw new Exception('Module is not reachable')
50
51        }
52
53        def samples = assay.samples
54
55        [   'Subject Data' :            getUsedTemplateFieldNames( samples*."parentSubject".unique() ),
56            'Sampling Event Data' :     getUsedTemplateFieldNames( samples*."parentEvent".unique() ),
57            'Sample Data' :             getUsedTemplateFieldNames( samples ),
58            'Event Group' :             ['name'],
59            'Module Measurement Data':  requestModuleMeasurementNames(assay)
60        ]
61
62    }
63
64    /**
65     * Gathers all assay related data, including measurements from the module,
66     * into 1 hash map containing: Subject Data, Sampling Event Data, Sample
67     * Data, and module specific measurement data.
68     * Data from each of the 4 hash map entries are themselves hash maps
69     * representing a descriptive header (field name) as key and the data as
70     * value.
71     *
72     * @param assay the assay to collect data for
73     * @fieldMap map with categories as keys and fields as values
74     * @measurementTokens selection of measurementTokens
75     * @return The assay data structure as described above.
76     */
77    def collectAssayData(assay, fieldMap, measurementTokens) throws Exception {
78
79        def collectFieldValuesForTemplateEntities = { templateFieldNames, templateEntities ->
80
81            // return a hash map with for each field name all values from the
82            // template entity list
83            templateFieldNames.inject([:]) { map, fieldName ->
84
85                map + [(fieldName): templateEntities.collect {
86
87                    it?.fieldExists(fieldName) ? it.getFieldValue(fieldName) : ''
88
89                }]
90
91            }
92
93        }
94
95        def getFieldValues = { templateEntities, fieldNames, propertyName = '' ->
96
97            def returnValue
98
99            // if no property name is given, simply collect the fields and
100            // values of the template entities themselves
101            if (propertyName == '') {
102
103                returnValue = collectFieldValuesForTemplateEntities(fieldNames, templateEntities)
104
105            } else {
106
107                // if a property name is given, we'll have to do a bit more work
108                // to ensure efficiency. The reason for this is that for a list
109                // of template entities, the properties referred to by
110                // propertyName can include duplicates. For example, for 10
111                // samples, there may be less than 10 parent subjects. Maybe
112                // there's only 1 parent subject. We don't want to collect field
113                // values for this single subject 10 times ...
114                def fieldValues
115
116                // we'll get the unique list of properties to make sure we're
117                // not getting the field values for identical template entity
118                // properties more then once.
119                def uniqueProperties = templateEntities*."$propertyName".unique()
120
121                fieldValues = collectFieldValuesForTemplateEntities(fieldNames, uniqueProperties)
122
123                // prepare a lookup hashMap to be able to map an entities'
124                // property (e.g. a sample's parent subject) to an index value
125                // from the field values list
126                int i = 0
127                def propertyToFieldValueIndexMap = uniqueProperties.inject([:]) { map, item -> map + [(item):i++]}
128
129                // prepare the return value so that it has an entry for field
130                // name. This will be the column name (second header line).
131                returnValue = fieldNames.inject([:]) { map, item -> map + [(item):[]] }
132
133                // finally, fill map the unique field values to the (possibly
134                // not unique) template entity properties. In our example with
135                // 1 unique parent subject, this means copying that subject's
136                // field values to all 10 samples.
137                templateEntities.each{ te ->
138
139                    fieldNames.each{
140
141                        returnValue[it] << fieldValues[it][propertyToFieldValueIndexMap[te[propertyName]]]
142
143                    }
144
145                }
146
147            }
148
149            returnValue
150
151        }
152
153        // check whether module is reachable
154        if (!moduleCommunicationService.isModuleReachable(assay.module.url)) {
155
156            throw new Exception('Module is not reachable')
157
158        }
159
160        def samples = assay.samples
161
162        def eventFieldMap = [:]
163
164        // check whether event group data was requested
165        if (fieldMap['Event Group']) {
166
167            def names = samples*.parentEventGroup*.name.flatten()
168
169            // only set name field when there's actual data
170            if (!names.every {!it}) eventFieldMap['name'] = names
171
172        }
173
174        [   'Subject Data' :            getFieldValues(samples, fieldMap['Subject Data'], 'parentSubject'),
175            'Sampling Event Data' :     getFieldValues(samples, fieldMap['Sampling Event Data'], 'parentEvent'),
176            'Sample Data' :             getFieldValues(samples, fieldMap['Sample Data']),
177            'Event Group' :             eventFieldMap,
178            'Module Measurement Data':  measurementTokens ? requestModuleMeasurements(assay, measurementTokens) : [:]
179        ]
180    }
181
182    /**
183     * Retrieves measurement names from the module through a rest call
184     *
185     * @param consumer the url of the module
186     * @param path path of the rest call to the module
187     * @return
188     */
189    def requestModuleMeasurementNames(assay) {
190
191        def moduleUrl = assay.module.url
192
193        def path = moduleUrl + "/rest/getMeasurementMetaData?assayToken=$assay.assayUUID"
194
195        moduleCommunicationService.callModuleRestMethodJSON(moduleUrl, path)
196
197    }
198
199    /**
200     * Retrieves module measurement data through a rest call to the module
201     *
202     * @param consumer the url of the module
203     * @param path path of the rest call to the module
204     * @return
205     */
206    def requestModuleMeasurements(assay, fields) {
207
208        def moduleUrl = assay.module.url
209
210        def tokenString = ''
211
212        fields.each{tokenString+="&measurementToken=${it.name.encodeAsURL()}"}
213
214        def path = moduleUrl + "/rest/getMeasurementData?assayToken=$assay.assayUUID" + tokenString
215       
216        def (sampleTokens, measurementTokens, moduleData) = moduleCommunicationService.callModuleRestMethodJSON(moduleUrl, path)
217
218        if (!sampleTokens?.size()) return []
219
220        def lastDataIndex   = moduleData.size() - 1
221        def stepSize        = sampleTokens.size() + 1
222
223        // Transpose the data to order it by measurement (compound) so it can be
224        // written as 1 column
225        int i = 0
226        measurementTokens.inject([:]) { map, token ->
227
228            map + [(token): moduleData[(i++..lastDataIndex).step(stepSize)]]
229
230        }
231
232    }
233
234    /**
235     * Converts column
236     * @param columnData multidimensional map containing column data.
237     * On the top level, the data must be grouped by category. Each key is the
238     * category title and the values are maps representing the columns. Each
239     * column also has a title (its key) and a list of values. Columns must be
240     * equally sized.
241     *
242     * For example, consider the following map:
243     * [Category1:
244     *      [Column1: [1,2,3], Column2: [4,5,6]],
245     *  Category2:
246     *      [Column3: [7,8,9], Column4: [10,11,12], Column5: [13,14,15]]]
247     *
248     * which will be written as:
249     *
250     * | Category1  |           | Category2 |           |           |
251     * | Column1    | Column2   | Column3   | Column4   | Column5   |
252     * | 1          | 4         | 7         | 10        | 13        |
253     * | 2          | 5         | 8         | 11        | 14        |
254     * | 3          | 6         | 9         | 12        | 15        |
255     *
256     * @return row wise data
257     */
258    def convertColumnToRowStructure(columnData) {
259
260            // check if all columns have the dimensionality 2
261            if (columnData.every { it.value.every { it.value instanceof ArrayList } }) {
262
263                def headers = [[],[]]
264
265                columnData.each { category ->
266
267                    if (category.value.size()) {
268
269                        // put category keys into first row separated by null values
270                        // wherever there are > 1 columns per category
271                        headers[0] += [category.key] + [null] * (category.value.size() - 1)
272
273                        // put non-category column headers into 2nd row
274                        headers[1] += category.value.collect{it.key}
275
276                    }
277
278                }
279
280                def d = []
281
282                // add all column wise data into 'd'
283                columnData.each { it.value.each { d << it.value } }
284
285                // transpose d into row wise data and combine with header rows
286                headers + d.transpose()
287            }
288
289        }
290
291    /**
292     * Export column wise data in Excel format to a stream.
293     *
294     * @param columnData Multidimensional map containing column data
295     * @param outputStream Stream to write to
296     * @param useOfficeOpenXML Flag to specify xlsx (standard) or xls output
297     * @return
298     */
299    def exportColumnWiseDataToExcelFile(columnData, outputStream, useOfficeOpenXML = true) {
300
301        // transform data into row based structure for easy writing
302        def rows = convertColumnToRowStructure(columnData)
303
304        if (rows) {
305
306            exportRowWiseDataToExcelFile(rows, outputStream, useOfficeOpenXML)
307
308        } else {
309
310            throw new Exception('Wrong column data format.')
311
312        }
313
314    }
315
316    /**
317     * Export row wise data in Excel format to a stream
318     *
319     * @param rowData List of lists containing for each row all cell values
320     * @param outputStream Stream to write to
321     * @param useOfficeOpenXML Flag to specify xlsx (standard) or xls output
322     * @return
323     */
324    def exportRowWiseDataToExcelFile(rowData, outputStream, useOfficeOpenXML = true) {
325
326        Workbook wb = useOfficeOpenXML ? new XSSFWorkbook() : new HSSFWorkbook()
327        Sheet sheet = wb.createSheet()
328
329        // create all rows
330        rowData.size().times { sheet.createRow it }
331
332        sheet.eachWithIndex { Row row, ri ->
333
334            // create appropriate number of cells for this row
335            rowData[ri].size().times { row.createCell it }
336
337            row.eachWithIndex { Cell cell, ci ->
338
339                // Numbers and values of type boolean, String, and Date can be
340                // written as is, other types need converting to String
341                def value = rowData[ri][ci]
342
343                value = (value instanceof Number | value?.class in [boolean.class, String.class, Date.class]) ? value : value?.toString()
344
345                // write the value (or an empty String if null) to the cell
346                cell.setCellValue(value ?: '')
347
348            }
349
350        }
351
352        wb.write(outputStream)
353        outputStream.close()
354
355    }
356
357}
Note: See TracBrowser for help on using the browser.