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

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

Rudimentary assay export functionality

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