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

Last change on this file since 1716 was 1716, checked in by s.h.sikkema@…, 9 years ago

new gdtImporter; new jumpbar; excel export to webflow; fixed spelling errors on home page; added menu entry for new gdtImporter

  • Property svn:keywords set to Rev Author Date
File size: 12.3 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: 1716 $
10 * $Author: s.h.sikkema@gmail.com $
11 * $Date: 2011-04-06 14:40:13 +0000 (wo, 06 apr 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
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        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']*.name, 'parentSubject'),
175            'Sampling Event Data' :     getFieldValues(samples, fieldMap['Sampling Event Data']*.name, 'parentEvent'),
176            'Sample Data' :             getFieldValues(samples, fieldMap['Sample Data']*.name),
177            'Event Group' :             eventFieldMap,
178            'Module Measurement Data':  measurementTokens*.name ? 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/query?assayToken=$assay.assayUUID"
194
195        def jsonArray = moduleCommunicationService.callModuleRestMethodJSON(moduleUrl, path)
196
197        // convert the JSONArray of JSONObjects to an array of hash maps
198        jsonArray.collect{ jo -> // JSONObject
199            [(jo.keys()[0]): jo.values().toList()[0]]
200        }
201
202    }
203
204    /**
205     * Retrieves module measurement data through a rest call to the module
206     *
207     * @param consumer the url of the module
208     * @param path path of the rest call to the module
209     * @return
210     */
211    def requestModuleMeasurements(assay, fields) {
212
213        def moduleUrl = assay.module.url
214
215        def tokenString = ''
216
217        fields.each{
218            tokenString+="&measurementToken=${it.name.encodeAsURL()}"
219        }
220       
221        def path = moduleUrl + "/rest/getMeasurementData/query?assayToken=$assay.assayUUID" + tokenString
222       
223        def (sampleTokens, measurementTokens, moduleData) = moduleCommunicationService.callModuleRestMethodJSON(moduleUrl, path)
224
225        if (!sampleTokens?.size()) return []
226
227        def lastDataIndex   = moduleData.size() - 1
228        def stepSize        = sampleTokens.size() + 1
229
230        // Transpose the data to order it by measurement (compound) so it can be
231        // written as 1 column
232        int i = 0
233        measurementTokens.inject([:]) { map, token ->
234
235            map + [(token): moduleData[(i++..lastDataIndex).step(stepSize)]]
236
237        }
238
239    }
240
241    /**
242     * Converts column
243     * @param columnData multidimensional map containing column data.
244     * On the top level, the data must be grouped by category. Each key is the
245     * category title and the values are maps representing the columns. Each
246     * column also has a title (its key) and a list of values. Columns must be
247     * equally sized.
248     *
249     * For example, consider the following map:
250     * [Category1:
251     *      [Column1: [1,2,3], Column2: [4,5,6]],
252     *  Category2:
253     *      [Column3: [7,8,9], Column4: [10,11,12], Column5: [13,14,15]]]
254     *
255     * which will be written as:
256     *
257     * | Category1  |           | Category2 |           |           |
258     * | Column1    | Column2   | Column3   | Column4   | Column5   |
259     * | 1          | 4         | 7         | 10        | 13        |
260     * | 2          | 5         | 8         | 11        | 14        |
261     * | 3          | 6         | 9         | 12        | 15        |
262     *
263     * @return row wise data
264     */
265    def convertColumnToRowStructure(columnData) {
266
267            // check if all columns have the dimensionality 2
268            if (columnData.every { it.value.every { it.value instanceof ArrayList } }) {
269
270                def headers = [[],[]]
271
272                columnData.each { category ->
273
274                    if (category.value.size()) {
275
276                        // put category keys into first row separated by null values
277                        // wherever there are > 1 columns per category
278                        headers[0] += [category.key] + [null] * (category.value.size() - 1)
279
280                        // put non-category column headers into 2nd row
281                        headers[1] += category.value.collect{it.key}
282
283                    }
284
285                }
286
287                def d = []
288
289                // add all column wise data into 'd'
290                columnData.each { it.value.each { d << it.value } }
291
292                // transpose d into row wise data and combine with header rows
293                headers + d.transpose()
294            }
295
296        }
297
298    /**
299     * Export column wise data in Excel format to a stream.
300     *
301     * @param columnData Multidimensional map containing column data
302     * @param outputStream Stream to write to
303     * @param useOfficeOpenXML Flag to specify xlsx (standard) or xls output
304     * @return
305     */
306    def exportColumnWiseDataToExcelFile(columnData, outputStream, useOfficeOpenXML = true) {
307
308        // transform data into row based structure for easy writing
309        def rows = convertColumnToRowStructure(columnData)
310
311        if (rows) {
312
313            exportRowWiseDataToExcelFile(rows, outputStream, useOfficeOpenXML)
314
315        } else {
316
317            throw new Exception('Wrong column data format.')
318
319        }
320
321    }
322
323    /**
324     * Export row wise data in Excel format to a stream
325     *
326     * @param rowData List of lists containing for each row all cell values
327     * @param outputStream Stream to write to
328     * @param useOfficeOpenXML Flag to specify xlsx (standard) or xls output
329     * @return
330     */
331    def exportRowWiseDataToExcelFile(rowData, outputStream, useOfficeOpenXML = true) {
332
333        Workbook wb = useOfficeOpenXML ? new XSSFWorkbook() : new HSSFWorkbook()
334        Sheet sheet = wb.createSheet()
335
336        // create all rows
337        rowData.size().times { sheet.createRow it }
338
339        sheet.eachWithIndex { Row row, ri ->
340
341            // create appropriate number of cells for this row
342            rowData[ri].size().times { row.createCell it }
343
344            row.eachWithIndex { Cell cell, ci ->
345
346                // Numbers and values of type boolean, String, and Date can be
347                // written as is, other types need converting to String
348                def value = rowData[ri][ci]
349
350                value = (value instanceof Number | value?.class in [boolean.class, String.class, Date.class]) ? value : value?.toString()
351
352                // write the value (or an empty String if null) to the cell
353                cell.setCellValue(value ?: '')
354
355            }
356
357        }
358
359        wb.write(outputStream)
360        outputStream.close()
361
362    }
363
364}
Note: See TracBrowser for help on using the repository browser.