root/trunk/grails-app/services/dbnp/importer/ImporterService.groovy @ 1121

Revision 1121, 20.1 KB (checked in by t.w.abma@…, 3 years ago)

- rewrote part of the importer and added a ImportRecord? and ImportCell? domain
- ontologies should now always be shown individually

  • Property svn:keywords set to Date Author Rev
Line 
1/**
2 * Importer service
3 *
4 * The importer service handles the import of tabular, comma delimited and Excel format
5 * based files.
6 *
7 * @package     importer
8 * @author      t.w.abma@umcutrecht.nl
9 * @since       20100126
10 *
11 * Revision information:
12 * $Rev$
13 * $Author$
14 * $Date$
15 */
16
17package dbnp.importer
18import org.apache.poi.ss.usermodel.*
19
20import dbnp.studycapturing.TemplateFieldType
21import dbnp.studycapturing.Template
22import dbnp.studycapturing.SamplingEvent
23import dbnp.studycapturing.Study
24import dbnp.studycapturing.Subject
25import dbnp.studycapturing.Event
26import dbnp.studycapturing.Sample
27
28class ImporterService {
29    def AuthenticationService
30
31    boolean transactional = true
32
33    /**
34    * @param is input stream representing the (workbook) resource
35    * @return high level representation of the workbook
36    */
37    Workbook getWorkbook(InputStream is) {
38        WorkbookFactory.create(is)
39    }
40
41    /**
42     * @param wb high level representation of the workbook
43     * @param sheetindex sheet to use within the workbook
44     * @return header representation as a MappingColumn hashmap
45     */
46    def getHeader(Workbook wb, int sheetindex, int headerrow, int datamatrix_start, theEntity=null){
47
48        def sheet = wb.getSheetAt(sheetindex)   
49        def sheetrow = sheet.getRow(datamatrix_start)
50        //def header = []
51        def header = [:]
52        def df = new DataFormatter()
53        def property = new String()
54
55        //for (Cell c: sheet.getRow(datamatrix_start)) {
56
57        (0..sheetrow.getLastCellNum() -1 ).each { columnindex ->
58
59            //def index =   c.getColumnIndex()
60            def datamatrix_celltype = sheet.getRow(datamatrix_start).getCell(columnindex,Row.CREATE_NULL_AS_BLANK).getCellType()
61            def datamatrix_celldata = df.formatCellValue(sheet.getRow(datamatrix_start).getCell(columnindex))
62            def datamatrix_cell     = sheet.getRow(datamatrix_start).getCell(columnindex)           
63            def headercell = sheet.getRow(headerrow-1+sheet.getFirstRowNum()).getCell(columnindex)
64            def tft = TemplateFieldType.STRING //default templatefield type
65
66            // Check for every celltype, currently redundant code, but possibly this will be
67            // a piece of custom code for every cell type like specific formatting         
68               
69            switch (datamatrix_celltype) {
70                    case Cell.CELL_TYPE_STRING:
71                            //parse cell value as double
72                            def doubleBoolean = true
73                            def fieldtype = TemplateFieldType.STRING
74
75                            // is this string perhaps a double?
76                            try {
77                                formatValue(datamatrix_celldata, TemplateFieldType.DOUBLE)
78                            } catch (NumberFormatException nfe) { doubleBoolean = false }
79                            finally {
80                                if (doubleBoolean) fieldtype = TemplateFieldType.DOUBLE
81                            }
82
83                            header[columnindex] = new dbnp.importer.MappingColumn(name:df.formatCellValue(headercell),
84                                                                            templatefieldtype:fieldtype,
85                                                                            index:columnindex,
86                                                                            entity:theEntity,
87                                                                            property:property);
88
89                            break
90                    case Cell.CELL_TYPE_NUMERIC:
91                            def fieldtype = TemplateFieldType.INTEGER
92                            def doubleBoolean = true
93                            def integerBoolean = true
94
95                            // is this cell really an integer?
96                            try {
97                                Integer.valueOf(datamatrix_celldata)
98                            } catch (NumberFormatException nfe) { integerBoolean = false }
99                            finally {
100                                if (integerBoolean) fieldtype = TemplateFieldType.INTEGER
101                            }
102
103                            // it's not an integer, perhaps a double?
104                            if (!integerBoolean)
105                                try {
106                                    formatValue(datamatrix_celldata, TemplateFieldType.DOUBLE)
107                                } catch (NumberFormatException nfe) { doubleBoolean = false }
108                                finally {
109                                    if (doubleBoolean) fieldtype = TemplateFieldType.DOUBLE
110                                }
111
112                            if (DateUtil.isCellDateFormatted(datamatrix_cell)) fieldtype = TemplateFieldType.DATE
113
114                            header[columnindex] = new dbnp.importer.MappingColumn(name:df.formatCellValue(headercell),
115                                                                            templatefieldtype:fieldtype,
116                                                                            index:columnindex,
117                                                                            entity:theEntity,
118                                                                            property:property);
119                            break
120                    case Cell.CELL_TYPE_BLANK:
121                            header[columnindex] = new dbnp.importer.MappingColumn(name:df.formatCellValue(headercell),
122                                                                            templatefieldtype:TemplateFieldType.STRING,
123                                                                            index:columnindex,
124                                                                            entity:theEntity,
125                                                                            property:property);
126                            break
127                    default:
128                            header[columnindex] = new dbnp.importer.MappingColumn(name:df.formatCellValue(headercell),
129                                                                            templatefieldtype:TemplateFieldType.STRING,
130                                                                            index:columnindex,
131                                                                            entity:theEntity,
132                                                                            property:property);
133                            break
134            } // end of switch
135        } // end of cell loop
136        return header
137    }
138
139    /**
140     * This method is meant to return a matrix of the rows and columns
141     * used in the preview
142     *
143     * @param wb workbook object
144     * @param sheetindex sheet index used
145     * @param rows amount of rows returned
146     * @return two dimensional array (matrix) of Cell objects
147     */
148
149    Cell[][] getDatamatrix(Workbook wb, header, int sheetindex, int datamatrix_start, int count) {
150        def sheet = wb.getSheetAt(sheetindex)
151        def rows  = []
152        def df = new DataFormatter()   
153
154        // walk through all rows
155        (count <= sheet.getLastRowNum()) ?
156        ((datamatrix_start+sheet.getFirstRowNum())..count).each { rowindex ->
157            def row = []
158
159            // walk through every cell
160            /*for (Cell c: sheet.getRow(rowindex)) {
161                row.add(c)
162                println c.getColumnIndex() + "=" +c
163            }*/
164           
165            (0..header.size()-1).each { columnindex ->
166                def c = sheet.getRow(rowindex).getCell(columnindex, Row.CREATE_NULL_AS_BLANK)
167                //row.add(df.formatCellValue(c))
168                row.add(c)
169                //if (c.getCellType() == c.CELL_TYPE_STRING) println "STR"+c.getStringCellValue()
170                //if (c.getCellType() == c.CELL_TYPE_NUMERIC) println "INT" +c.getNumericCellValue()
171            }
172                //row.add(df.formatCellValue(c))
173            rows.add(row)
174        } : 0
175
176        return rows
177    }
178
179    /**
180    * This method will move a file to a new location.
181    *
182    * @param file File object to move
183    * @param folderpath folder to move the file to
184    * @param filename (new) filename to give
185    * @return if file has been moved succesful, the new path and filename will be returned, otherwise an empty string will be returned
186    */
187    def moveFile(File file, String folderpath, String filename) {
188        try {
189                def rnd = ""; //System.currentTimeMillis()
190                file.transferTo(new File(folderpath, rnd+filename))
191                return folderpath + filename
192            } catch(Exception exception) {
193                log.error "File move error, ${exception}"
194                return ""
195                }
196    }
197
198    /**
199    * @return random numeric value
200    */
201    def random = {
202            return System.currentTimeMillis() + Runtime.runtime.freeMemory()
203        }
204
205    /**
206    * Method to read data from a workbook and to import data into a two dimensional
207    * array
208    *
209    * @param template_id template identifier to use fields from
210    * @param wb POI horrible spreadsheet formatted workbook object
211    * @param mcmap linked hashmap (preserved order) of MappingColumns
212    * @param sheetindex sheet to use when using multiple sheets
213    * @param rowindex first row to start with reading the actual data (NOT the header)
214    * @return two dimensional array containing records (with entities)
215    *
216    * @see dbnp.importer.MappingColumn
217    */
218    def importData(template_id, Workbook wb, int sheetindex, int rowindex, mcmap) {
219        def sheet = wb.getSheetAt(sheetindex)
220        def table = []
221        def failedcells = [:] // map [recordhash, importrecord] values
222       
223        // walk through all rows and fill the table with records
224        (rowindex..sheet.getLastRowNum()).each { i ->       
225            // Create an entity record based on a row read from Excel and store the cells which failed to be mapped
226            def (record, failed) = createRecord(template_id, sheet.getRow(i), mcmap)
227
228            // Add record with entity and its values to the table
229            table.add(record)
230
231            // If failed cells have been found, add them to the failed cells map
232            // the record hashcode is later on used to put the failed data back
233            // in the data matrix           
234            if (failed.importcells.size()>0) failedcells.put(record.hashCode(), failed)
235        }
236
237        return [table,failedcells]
238    }
239
240    /** Method to put failed cells back into the datamatrix. Failed cells are cell values
241     * which could not be stored in an entity (e.g. Humu Supiuns in an ontology field).
242     * Empty corrections should not be stored
243     *
244     * @param datamatrix two dimensional array containing entities and possibly also failed cells
245     * @param failedcells list with maps of failed cells in [mappingcolumn, cell] format
246     * @param correctedcells map of corrected cells in [cellhashcode, value] format
247     **/
248    def saveCorrectedCells(datamatrix, failedcells, correctedcells) {       
249       
250        // Loop through all failed cells (stored as
251        failedcells.each { record ->
252            record.value.importcells.each { cell ->
253
254                  // Get the corrected value
255                  def correctedvalue = correctedcells.find { it.key.toInteger() == cell.getIdentifier()}.value
256
257                  // Find the record in the table which the mappingcolumn belongs to
258                  def tablerecord = datamatrix.find { it.hashCode() == record.key }
259
260                  // Loop through all entities in the record and correct them if necessary
261                  tablerecord.each { rec ->
262                      rec.each { entity ->
263                            try {
264                                // Update the entity field
265                                entity.setFieldValue(cell.mappingcolumn.property, correctedvalue)
266                                //println "Adjusted " + cell.mappingcolumn.property + " to " + correctedvalue
267                            }
268                            catch (Exception e) {
269                                //println "Could not map corrected ontology: " + cell.mappingcolumn.property + " to " + correctedvalue
270                            }
271                      }
272                  } // end of table record
273               } // end of cell record
274            } // end of failedlist
275    }
276   
277    /**
278     * Method to store a matrix containing the entities in a record like structure. Every row in the table
279     * contains one or more entity objects (which contain fields with values). So actually a row represents
280     * a record with fields from one or more different entities.
281     *
282     * @param study entity Study
283     * @param datamatrix two dimensional array containing entities with values read from Excel file
284     */   
285    def saveDatamatrix(Study study, datamatrix) {
286        def validatedSuccesfully = 0
287        def entitystored = null
288        study.refresh()       
289       
290        // go through the data matrix, read every record and validate the entity and try to persist it
291        datamatrix.each { record ->
292            record.each { entity ->
293                        switch (entity.getClass()) {
294                        case Study       :  print "Persisting Study `" + entity + "`: "
295                                                entity.owner = AuthenticationService.getLoggedInUser()
296                                                if (persistEntity(entity)) validatedSuccesfully++
297                                                break
298                        case Subject     :  print "Persisting Subject `" + entity + "`: "
299                                                entity.parent = study
300                                               
301                                                // is the current entity not already in the database?
302                                                entitystored = isEntityStored(entity)
303                                               
304                                                // this entity is new, so add it to the study
305                                                if (entitystored==null) study.addToSubjects(entity)
306                                                else // existing entity, so update it
307                                                    updateEntity(entitystored, entity)
308
309                                                if (persistEntity(study)) validatedSuccesfully++
310                                                break
311                        case Event       :  print "Persisting Event `" + entity + "`: "
312                                                entity.parent = study
313                                                study.addToEvents(entity)
314                                                if (persistEntity(entity)) validatedSuccesfully++
315                                                break
316                        case Sample      :  print "Persisting Sample `" + entity +"`: "
317                                                entity.parent = study
318                                                study.addToSamples(entity)
319                                                if (persistEntity(entity)) validatedSuccesfully++
320                                                break
321                        case SamplingEvent: print "Persisting SamplingEvent `" + entity + "`: "
322                                                entity.parent = study
323                                                study.addToSamplingEvents(entity)
324                                                if (persistEntity(entity)) validatedSuccesfully++
325                                                break
326                        default          :  println "Skipping persisting of `" + entity.getclass() +"`"
327                                                break
328                        } // end switch
329            } // end record
330        } // end datamatrix
331        return validatedSuccesfully
332    }
333
334    /**
335     * Check whether an entity already exist. A unique field in the entity is
336     * used to check whether the instantiated entity (read from Excel) is new.
337     * If the entity is found in the database it will be returned as is.
338     *
339     * @param entity entity object like a Study, Subject, Sample et cetera
340     * @return entity if found, otherwise null
341     */
342    def isEntityStored(entity) {
343            switch (entity.getClass()) {
344                        case Study          :  return Study.findByCode(entity.code)
345                                               break
346                        case Subject        :  return Subject.findByParentAndName(entity.parent, entity.name)
347                                               break
348                        case Event          :  break
349                        case Sample         :  break
350                        case SamplingEvent  :  break
351                        default             :  // unknown entity
352                                               return null
353            }
354    }
355
356    /**
357     * Find the entity and update the fields. The entity is an instance
358     * read from Excel. This method looks in the database for the entity
359     * having the same identifier. If it has found the same entity
360     * already in the database, it will update the record.
361     *
362     * @param entitystored existing record in the database to update
363     * @param entity entity read from Excel
364     */
365    def updateEntity(entitystored, entity) {
366        switch (entity.getClass()) {
367                        case Study          :  break
368                        case Subject        :  entitystored.properties = entity.properties
369                                               entitystored.save()
370                                               break
371                        case Event          :  break
372                        case Sample         :  break
373                        case SamplingEvent  :  break
374                        default             :  // unknown entity
375                                               return null
376        }
377    }
378
379    /**
380     * Method to persist entities into the database
381     * Checks whether entity already exists (based on identifier column 'name')
382     *
383     * @param entity entity object like Study, Subject, Protocol et cetera
384     *
385     */
386    boolean persistEntity(entity) {
387            println "persisting ${entity}"           
388            // if not validated
389                if (entity.validate()) {
390                        if (entity.save()) { //.merge?
391                                return true
392                        }
393                        else { // if save was unsuccesful
394                                entity.errors.allErrors.each {
395                                        println it
396                                }
397                                return false
398                        }
399                }
400            else { // if not validated
401                    entity.errors.each {
402                            println it
403                    }
404                        return false
405            }
406         }
407
408        /**
409         * This method creates a record (array) containing entities with values
410         *
411         * @param template_id template identifier
412         * @param excelrow POI based Excel row containing the cells
413         * @param mcmap map containing MappingColumn objects
414         * @return list of entities and list of failed cells
415         */
416        def createRecord(template_id, Row excelrow, mcmap) {
417                def df = new DataFormatter()
418                def template = Template.get(template_id)
419                def tft = TemplateFieldType
420                def record = [] // list of entities and the read values
421                def failed = new ImportRecord() // list of failed cells with the value which couldn't be mapped into the entity
422
423                // Initialize all possible entities with the chosen template
424                def study = new Study(template: template)
425                def subject = new Subject(template: template)
426                def samplingEvent = new SamplingEvent(template: template)
427                def event = new Event(template: template)
428                def sample = new Sample(template: template)
429
430                // Go through the Excel row cell by cell
431                for (Cell cell: excelrow) {
432                        // get the MappingColumn information of the current cell
433                        def mc = mcmap[cell.getColumnIndex()]
434                        def value                       
435
436                        // Check if column must be imported
437                        if (mc!=null) if (!mc.dontimport) {
438                                try {
439                                        value = formatValue(df.formatCellValue(cell), mc.templatefieldtype)
440                                } catch (NumberFormatException nfe) {
441                                        value = ""
442                                }
443
444
445                                //println "temateplfedielfdtype=" + mc.templatefieldtype
446                                // Are we trying to map an ontology term which is empty? Then it is a failed cell
447                                /*if (value=="") {
448                                    println "empty term"
449                                    def temp = new MappingColumn()
450                                    //temp.properties = mc.properties
451                                    temp.value = "undefined"
452                                    failed.add(temp)
453                                }*/                               
454                             
455
456                                try {
457                                // which entity does the current cell (field) belong to?
458                                    switch (mc.entity) {
459                                        case Study: // does the entity already exist in the record? If not make it so.
460                                                (record.any {it.getClass() == mc.entity}) ? 0 : record.add(study)
461                                                study.setFieldValue(mc.property, value)
462                                                break
463                                        case Subject: (record.any {it.getClass() == mc.entity}) ? 0 : record.add(subject)
464                                                subject.setFieldValue(mc.property, value)
465                                                break
466                                        case SamplingEvent: (record.any {it.getClass() == mc.entity}) ? 0 : record.add(samplingEvent)
467                                                samplingEvent.setFieldValue(mc.property, value)
468                                                break
469                                        case Event: (record.any {it.getClass() == mc.entity}) ? 0 : record.add(event)
470                                                event.setFieldValue(mc.property, value)
471                                                break
472                                        case Sample: (record.any {it.getClass() == mc.entity}) ? 0 : record.add(sample)
473                                                sample.setFieldValue(mc.property, value)
474                                                break
475                                        case Object:   // don't import
476                                                break
477                                    } // end switch
478                                } catch (IllegalArgumentException iae) {
479                                    // store the mapping column and value which failed
480                                    def mcInstance = new MappingColumn()
481                                    mcInstance.properties = mc.properties
482                                    failed.addToImportcells(
483                                        new ImportCell(mappingcolumn:mcInstance,
484                                            value:value)
485                                        )
486                                }
487                        } // end
488                } // end for
489        // a failed column means that using the entity.setFieldValue() threw an exception       
490        return [record, failed]       
491    }
492
493    /**
494    * Method to parse a value conform a specific type
495    * @param value string containing the value
496    * @return object corresponding to the TemplateFieldType
497    */
498    def formatValue(String value, TemplateFieldType type) throws NumberFormatException {
499            switch (type) {
500                case TemplateFieldType.STRING       :   return value.trim()
501                case TemplateFieldType.TEXT         :   return value.trim()
502                case TemplateFieldType.INTEGER      :   return (int) Double.valueOf(value)
503                case TemplateFieldType.FLOAT        :   return Float.valueOf(value.replace(",","."));
504                case TemplateFieldType.DOUBLE       :   return Double.valueOf(value.replace(",","."));
505                case TemplateFieldType.STRINGLIST   :   return value.trim()
506                case TemplateFieldType.ONTOLOGYTERM :   return value.trim()
507                case TemplateFieldType.DATE         :   return value
508                default                             :   return value
509            }
510    }
511
512}
Note: See TracBrowser for help on using the browser.