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

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

Using moduleCommunicationService now to fetch measurement data from module

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