source: trunk/grails-app/services/dbnp/importer/ImporterService.groovy @ 1456

Last change on this file since 1456 was 1456, checked in by business@…, 11 years ago

moved gdt package to org.dbnp, moved tests for RelTime?, TemplateEntity?, Template etc. to gdt plugin

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