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

Revision 1731, 13.5 KB (checked in by robert@…, 3 years ago)

Bugfix with the order of samples in the assay exporter

  • Property svn:keywords set to Rev Author Date
Line 
1/**
2 * AssayService Service
3 *
4 * @author  s.h.sikkema@gmail.com
5 * @since       20101216
6 * @package     dbnp.studycapturing
7 *
8 * Revision information:
9 * $Rev$
10 * $Author$
11 * $Date$
12 */
13package dbnp.studycapturing
14
15import org.apache.poi.ss.usermodel.*
16import org.apache.poi.xssf.usermodel.XSSFWorkbook
17import org.apache.poi.hssf.usermodel.HSSFWorkbook
18
19class AssayService {
20
21    boolean transactional = false
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 getUsedTemplateFields = { 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            }.collect{[name: it.name, comment: it.comment]}
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' :            getUsedTemplateFields( samples*."parentSubject".unique() ),
56            'Sampling Event Data' :     getUsedTemplateFields( samples*."parentEvent".unique() ),
57            'Sample Data' :             getUsedTemplateFields( samples ),
58            'Event Group' :             [[name: 'name', comment: 'Name of Event Group']],
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        // Find samples and sort by name
161                def samples = assay.samples.toList().sort { it.name }
162
163        def eventFieldMap = [:]
164
165        // check whether event group data was requested
166        if (fieldMap['Event Group']) {
167
168            def names = samples*.parentEventGroup*.name.flatten()
169
170            // only set name field when there's actual data
171            if (!names.every {!it}) eventFieldMap['name'] = names
172
173        }
174
175        [   'Subject Data' :            getFieldValues(samples, fieldMap['Subject Data']*.name, 'parentSubject'),
176            'Sampling Event Data' :     getFieldValues(samples, fieldMap['Sampling Event Data']*.name, 'parentEvent'),
177            'Sample Data' :             getFieldValues(samples, fieldMap['Sample Data']*.name),
178            'Event Group' :             eventFieldMap,
179            'Module Measurement Data':  measurementTokens*.name ? requestModuleMeasurements(assay, measurementTokens, samples) : [:]
180        ]
181    }
182
183    /**
184     * Retrieves measurement names from the module through a rest call
185     *
186     * @param consumer the url of the module
187     * @param path path of the rest call to the module
188     * @return
189     */
190    def requestModuleMeasurementNames(assay) {
191
192        def moduleUrl = assay.module.url
193
194        def path = moduleUrl + "/rest/getMeasurements/query?assayToken=$assay.assayUUID"
195
196        def jsonArray = moduleCommunicationService.callModuleRestMethodJSON(moduleUrl, path)
197
198        jsonArray*.toString()
199
200    }
201
202    /**
203     * Retrieves module measurement data through a rest call to the module
204     *
205     * @param assay             Assay for which the module measurements should be retrieved
206     * @param fields    List with the names of the fields to be retrieved. Format: [ [ name: 'measurementName1' ], [ name: 'measurementName2' ] ]
207     * @param samples   Samples for which the module
208     * @return
209     */
210    def requestModuleMeasurements(assay, fields, samples) {
211
212        def moduleUrl = assay.module.url
213
214        def tokenString = ''
215
216        fields.each{
217            tokenString+="&measurementToken=${it.name.encodeAsURL()}"
218        }
219       
220        def path = moduleUrl + "/rest/getMeasurementData/query?assayToken=$assay.assayUUID" + tokenString
221       
222        def (sampleTokens, measurementTokens, moduleData) = moduleCommunicationService.callModuleRestMethodJSON(moduleUrl, path)
223
224        if (!sampleTokens?.size()) return []
225
226                // Convert the three different maps into a map like:
227                //
228                // [ "measurement 1": [ value1, value2, value3 ],
229                //   "measurement 2": [ value4, value5, value6 ] ]
230                //
231                // The returned values should be in the same order as the given samples-list
232                def map = [:]
233                def numSampleTokens = sampleTokens.size();
234               
235                measurementTokens.eachWithIndex { measurementToken, measurementIndex ->
236                        def measurements = [];
237                        samples.each { sample ->
238
239                                // Do measurements for this sample exist? If not, a null value is returned
240                                // for this sample. Otherwise, the measurement is looked up in the list with
241                                // measurements, based on the sample token
242                                if( sampleTokens.collect{ it.toString() }.contains( sample.giveUUID() ) ) {
243                                        def tokenIndex = sampleTokens.indexOf( sample.giveUUID() );
244                                        def valueIndex = measurementIndex * numSampleTokens + tokenIndex;
245                                       
246                                        // If the module data is in the wrong format, show an error in the log file
247                                        // and return a null value for this measurement.
248                                        if( valueIndex >= moduleData.size() ) {
249                                                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"
250                                                measurements << null
251                                        }  else {
252                                                measurements << moduleData[ valueIndex ].toString();
253                                        }
254                                } else {
255                                        measurements << null
256                                }
257                        }
258                        map[ measurementToken.toString() ] = measurements
259                }
260
261                return map;
262    }
263
264    /**
265     * Converts column
266     * @param columnData multidimensional map containing column data.
267     * On the top level, the data must be grouped by category. Each key is the
268     * category title and the values are maps representing the columns. Each
269     * column also has a title (its key) and a list of values. Columns must be
270     * equally sized.
271     *
272     * For example, consider the following map:
273     * [Category1:
274     *      [Column1: [1,2,3], Column2: [4,5,6]],
275     *  Category2:
276     *      [Column3: [7,8,9], Column4: [10,11,12], Column5: [13,14,15]]]
277     *
278     * which will be written as:
279     *
280     * | Category1  |           | Category2 |           |           |
281     * | Column1    | Column2   | Column3   | Column4   | Column5   |
282     * | 1          | 4         | 7         | 10        | 13        |
283     * | 2          | 5         | 8         | 11        | 14        |
284     * | 3          | 6         | 9         | 12        | 15        |
285     *
286     * @return row wise data
287     */
288    def convertColumnToRowStructure(columnData) {
289
290            // check if all columns have the dimensionality 2
291            if (columnData.every { it.value.every { it.value instanceof ArrayList } }) {
292
293                def headers = [[],[]]
294
295                columnData.each { category ->
296
297                    if (category.value.size()) {
298
299                        // put category keys into first row separated by null values
300                        // wherever there are > 1 columns per category
301                        headers[0] += [category.key] + [null] * (category.value.size() - 1)
302
303                        // put non-category column headers into 2nd row
304                        headers[1] += category.value.collect{it.key}
305
306                    }
307
308                }
309
310                def d = []
311
312                // add all column wise data into 'd'
313                columnData.each { it.value.each { d << it.value } }
314
315                // transpose d into row wise data and combine with header rows
316                headers + d.transpose()
317            }
318
319        }
320
321    /**
322     * Export column wise data in Excel format to a stream.
323     *
324     * @param columnData Multidimensional map containing column data
325     * @param outputStream Stream to write to
326     * @param useOfficeOpenXML Flag to specify xlsx (standard) or xls output
327     * @return
328     */
329    def exportColumnWiseDataToExcelFile(columnData, outputStream, useOfficeOpenXML = true) {
330
331        // transform data into row based structure for easy writing
332        def rows = convertColumnToRowStructure(columnData)
333
334        if (rows) {
335
336            exportRowWiseDataToExcelFile(rows, outputStream, useOfficeOpenXML)
337
338        } else {
339
340            throw new Exception('Wrong column data format.')
341
342        }
343
344    }
345
346    /**
347     * Export row wise data in Excel format to a stream
348     *
349     * @param rowData List of lists containing for each row all cell values
350     * @param outputStream Stream to write to
351     * @param useOfficeOpenXML Flag to specify xlsx (standard) or xls output
352     * @return
353     */
354    def exportRowWiseDataToExcelFile(rowData, outputStream, useOfficeOpenXML = true) {
355
356        Workbook wb = useOfficeOpenXML ? new XSSFWorkbook() : new HSSFWorkbook()
357        Sheet sheet = wb.createSheet()
358
359        // create all rows
360        rowData.size().times { sheet.createRow it }
361
362        sheet.eachWithIndex { Row row, ri ->
363
364            // create appropriate number of cells for this row
365            rowData[ri].size().times { row.createCell it }
366
367            row.eachWithIndex { Cell cell, ci ->
368
369                // Numbers and values of type boolean, String, and Date can be
370                // written as is, other types need converting to String
371                def value = rowData[ri][ci]
372
373                value = (value instanceof Number | value?.class in [boolean.class, String.class, Date.class]) ? value : value?.toString()
374
375                // write the value (or an empty String if null) to the cell
376                cell.setCellValue(value ?: '')
377
378            }
379
380        }
381
382        wb.write(outputStream)
383        outputStream.close()
384
385    }
386
387}
Note: See TracBrowser for help on using the browser.