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

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