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

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