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

Last change on this file was 1945, checked in by robert@…, 8 years ago
  • Updated the import algorithm in the simpleWizard, so it won't be too slow with > 200 samples
  • Changed the warning added in r1944 so the user knows why the import will be slow
  • Property svn:keywords set to Rev Author Date
File size: 38.5 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: 1945 $
13 * $Author: work@osx.eu $
14 * $Date: 2011-06-29 08:10:48 +0000 (wo, 29 jun 2011) $
15 */
16package dbnp.importer
17
18import org.dbnp.gdt.*
19import org.apache.poi.ss.usermodel.*
20import dbnp.studycapturing.*
21
22class ImporterService {
23        def authenticationService
24        def sessionFactory
25
26        static transactional = false
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) {
73                                                doubleBoolean = false
74                                        }
75                                        finally {
76                                                if (doubleBoolean) fieldtype = TemplateFieldType.DOUBLE
77                                        }
78
79                                        header[columnindex] = new dbnp.importer.MappingColumn(name: df.formatCellValue(headercell),
80                                                        templatefieldtype: fieldtype,
81                                                        index: columnindex,
82                                                        entityclass: theEntity,
83                                                        property: property);
84
85                                        break
86                                case Cell.CELL_TYPE_NUMERIC:
87                                        def fieldtype = TemplateFieldType.LONG
88                                        def doubleBoolean = true
89                                        def longBoolean = true
90
91                                // is this cell really an integer?
92                                        try {
93                                                Long.valueOf(datamatrix_celldata)
94                                        } catch (NumberFormatException nfe) {
95                                                longBoolean = false
96                                        }
97                                        finally {
98                                                if (longBoolean) fieldtype = TemplateFieldType.LONG
99                                        }
100
101                                // it's not an long, perhaps a double?
102                                        if (!longBoolean)
103                                                try {
104                                                        formatValue(datamatrix_celldata, TemplateFieldType.DOUBLE)
105                                                } catch (NumberFormatException nfe) {
106                                                        doubleBoolean = false
107                                                }
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                                                        entityclass: 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                                        entityclass: 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                                        entityclass: 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        Object[][] getDatamatrix(Workbook wb, header, int sheetindex, int datamatrix_start, int count) {
149                def sheet = wb.getSheetAt(sheetindex)
150                def rows = []
151                def df = new DataFormatter()
152
153                count = (count < sheet.getLastRowNum()) ? count : sheet.getLastRowNum()
154
155                // walk through all rows
156                ((datamatrix_start + sheet.getFirstRowNum())..count).each { rowindex ->
157                        def row = []
158
159                        (0..header.size() - 1).each { columnindex ->
160                                if (sheet.getRow(rowindex))
161                                        row.add( sheet.getRow(rowindex).getCell(columnindex, Row.CREATE_NULL_AS_BLANK) )
162                        }
163
164                        rows.add(row)
165                }
166
167                return rows
168        }
169
170        /**
171         * This method will move a file to a new location.
172         *
173         * @param file File object to move
174         * @param folderpath folder to move the file to
175         * @param filename (new) filename to give
176         * @return if file has been moved succesful, the new path and filename will be returned, otherwise an empty string will be returned
177         */
178        def moveFile(File file, String folderpath, String filename) {
179                try {
180                        def rnd = ""; //System.currentTimeMillis()
181                        file.transferTo(new File(folderpath, rnd + filename))
182                        return folderpath + filename
183                } catch (Exception exception) {
184                        log.error "File move error, ${exception}"
185                        return ""
186                }
187        }
188
189        /**
190         * @return random numeric value
191         */
192        def random = {
193                return System.currentTimeMillis() + Runtime.runtime.freeMemory()
194        }
195
196
197        /**
198         * Retrieves records with sample, subject, samplingevent etc. from a study
199         * @param s             Study to retrieve records from
200         * @return              A list with hashmaps [ 'objects': [ 'Sample': .., 'Subject': .., 'SamplingEvent': .., 'Event': '.. ], 'templates': [], 'templateCombination': .. ]
201         */
202        protected def getRecords( Study s ) {
203                def records = [];
204
205                s.samples?.each {
206                        def record = [ 'objects': retrieveEntitiesBySample( it ) ];
207
208                        def templates = [:]
209                        def templateCombination = [];
210                        record.objects.each { entity ->
211                                templates[ entity.key ] = entity.value?.template
212                                if( entity.value?.template )
213                                        templateCombination << entity.key + ": " + entity.value?.template?.name;
214                        }
215
216                        record.templates = templates;
217                        record.templateCombination = templateCombination.join( ', ' )
218
219                        records << record
220                }
221
222                return records;
223        }
224
225        /**
226         * Returns a subject, event and samplingEvent that belong to this sample
227         * @param s             Sample to find the information for
228         * @return
229         */
230        protected retrieveEntitiesBySample( Sample s ) {
231                return [
232                        'Subject': s?.parentSubject,
233                        'SamplingEvent': s?.parentEvent,
234                        'Event': s?.parentEventGroup?.events?.toList()?.getAt(0),
235                        'Sample': s
236                ]
237        }
238
239        /**
240         * Imports data from a workbook into a list of ImportRecords. If some entities are already in the database,
241         * these records are updated.
242         *
243         * This method is capable of importing Subject, Samples, SamplingEvents and Events
244         *
245         * @param       templates       Map of templates, identified by their entity as a key. For example: [ Subject: Template x, Sample: Template y ]
246         * @param       wb                      Excel workbook to import
247         * @param       sheetindex      Number of the sheet to import data from
248         * @param       rowindex        Row to start importing from.
249         * @param       mcmap           Hashmap of mappingcolumns, with the first entry in the hashmap containing information about the first column, etc.
250         * @param       parent          Study to import all data into. Is used for determining which sample/event/subject/assay to update
251         * @param       createAllEntities       If set to true, the system will also create objects for entities that have no data imported, but do have
252         *                                                              a template assigned
253         * @return      List            List with two entries:
254         *                      0                       List with ImportRecords, one for each row in the excelsheet
255         *                      1                       List with ImportCell objects, mentioning the cells that could not be correctly imported
256         *                                              (because the value in the excelsheet can't be entered into the template field)
257         */
258        def importOrUpdateDataBySampleIdentifier( def templates, Workbook wb, int sheetindex, int rowindex, def mcmap, Study parent = null, boolean createAllEntities = true ) {
259                if( !mcmap )
260                        return;
261                       
262                def hibernateSession = sessionFactory.getCurrentSession() 
263
264                // Check whether the rows should be imported in one or more entities
265                def entities
266                if( createAllEntities ) {
267                        entities = templates.entrySet().value.findAll { it }.entity;
268                } else {
269                        entities = mcmap.findAll{ !it.dontimport }.entityclass.unique();
270                }
271
272                def sheet = wb.getSheetAt(sheetindex)
273                def table = []
274                def samples = []
275                def failedcells = [] // list of cells that have failed to import
276               
277                // First check for each record whether an entity in the database should be updated,
278                // or a new entity should be added. This is done before any new object is created, since
279                // searching after new objects have been created (but not yet saved) will result in
280                //      org.hibernate.AssertionFailure: collection [...] was not processed by flush()
281                // errors
282                def existingEntities = [:]
283                for( int i = rowindex; i <= sheet.getLastRowNum(); i++ ) {
284                        existingEntities[i] = findExistingEntities( entities, sheet.getRow(i), mcmap, parent );
285                }
286
287                // walk through all rows and fill the table with records
288                for( int i = rowindex; i <= sheet.getLastRowNum(); i++ ) {
289                        def row = sheet.getRow(i);
290                       
291                        if( row && !rowIsEmpty( row ) ) {
292                                // Create an entity record based on a row read from Excel and store the cells which failed to be mapped
293                                def (record, failed) = importOrUpdateRecord( templates, entities, row, mcmap, parent, samples, existingEntities[i] );
294
295                                // Setup the relationships between the imported entities
296                                relateEntities( record );
297
298                                // Add record with entities and its values to the table
299                                table.add(record)
300
301                                // If failed cells have been found, add them to the failed cells list
302                                if (failed?.importcells?.size() > 0) failedcells.add(failed)
303                               
304                                // Add a sample to the list of samples if possible
305                                def sample = record.find { it instanceof dbnp.studycapturing.Sample }
306                                if( sample && !samples.contains( sample ) )
307                                        samples << sample
308                        }
309                }
310
311                return [ "table": table, "failedCells": failedcells ]
312        }
313       
314        /**
315         * Checks whether an excel row is empty
316         * @param row   Row from the excel sheet
317         * @return              True if all cells in this row are empty or the given row is null. False otherwise
318         */
319        def rowIsEmpty( Row excelRow ) {
320                if( !excelRow )
321                        return true;
322               
323                def df = new DataFormatter();
324                for( int i = excelRow.getFirstCellNum(); i < excelRow.getLastCellNum(); i++ ) {
325                        Cell cell = excelRow.getCell( i );
326                       
327                        try {
328                                def value = df.formatCellValue(cell)
329                                if( value )
330                                        return false
331                        } catch (NumberFormatException nfe) {
332                                // If the number can't be formatted, the row isn't empty
333                                return false;
334                        }
335                }
336               
337                return true;
338        }
339
340        /**
341         * Checks whether entities in the given row already exist in the database
342         * they are updated.
343         *
344         * @param       entities        Entities that have to be imported for this row
345         * @param       excelRow        Excel row to import into this record
346         * @param       mcmap           Hashmap of mappingcolumns, with the first entry in the hashmap containing information about the first column, etc.
347         * @return      Map                     Map with entities that have been found for this row. The key for the entities is the entity name (e.g.: [Sample: null, Subject: <subject object>]
348         */
349        def findExistingEntities(def entities, Row excelRow, mcmap, parent ) {
350                DataFormatter df = new DataFormatter();
351
352                // Find entities based on sample identifier
353                def sample = findEntityByRow( dbnp.studycapturing.Sample, excelRow, mcmap, parent, [], df );
354                return retrieveEntitiesBySample( sample );
355        }
356
357        /**
358         * Imports a records from the excelsheet into the database. If the entities are already in the database
359         * they are updated.
360         *
361         * This method is capable of importing Subject, Samples, SamplingEvents and Events
362         *
363         * @param       templates       Map of templates, identified by their entity as a key. For example: [ Sample: Template y ]
364         * @param       entities        Entities that have to be imported for this row
365         * @param       excelRow        Excel row to import into this record
366         * @param       mcmap           Hashmap of mappingcolumns, with the first entry in the hashmap containing information about the first column, etc.
367         * @param       parent          Study to import all data into. Is used for determining which sample/event/subject/assay to update
368         * @param       importedSamples Samples that have been imported before this row. These entities should be used again if possible,
369         *                                                      to avoid importing duplicates.
370         * @return      List            List with two entries:
371         *                      0                       List with ImportRecords, one for each row in the excelsheet
372         *                      1                       List with ImportCell objects, mentioning the cells that could not be correctly imported
373         *                                              (because the value in the excelsheet can't be entered into the template field)
374         */
375        def importOrUpdateRecord(def templates, def entities, Row excelRow, mcmap, Study parent = null, List importedSamples, Map existingEntities ) {
376                DataFormatter df = new DataFormatter();
377                def record = [] // list of entities and the read values
378                def failed = new ImportRecord() // map with entity identifier and failed mappingcolumn
379
380                def currentTime = System.currentTimeMillis();
381               
382                // Check whether this record mentions a sample that has been imported before. In that case,
383                // we update that record, in order to prevent importing the same sample multiple times
384                def importedEntities = [];
385                if( importedSamples )
386                        importedEntities = importedSamples
387
388                def importedSample = findEntityInImportedEntities( dbnp.studycapturing.Sample, excelRow, mcmap, importedEntities, df )
389                def imported = retrieveEntitiesBySample( importedSample );
390               
391                for( entity in entities ) {
392                        // Check whether this entity should be added or updated
393                        // The entity is updated is an entity with the same 'identifier' (field
394                        // specified to be the identifying field) is found in the database
395                        def entityName = entity.name[ entity.name.lastIndexOf( '.' ) + 1..-1];
396                        def template = templates[ entityName ];
397
398                        // If no template is specified for this entity, continue with the next
399                        if( !template )
400                                continue;
401
402                        // Check whether the object exists in the list of already imported entities
403                        def entityObject = imported[ entityName ]
404
405                        // If it doesn't, search for the entity in the database
406                        if( !entityObject && existingEntities )
407                                entityObject = existingEntities[ entityName ];
408
409                        // Otherwise, create a new object
410                        if( !entityObject )
411                                entityObject = entity.newInstance();
412
413                        // Update the template
414                        entityObject.template = template;
415
416                        // Go through the Excel row cell by cell
417                        for( int i = excelRow.getFirstCellNum(); i < excelRow.getLastCellNum(); i++ ) {
418                                Cell cell = excelRow.getCell( i );
419                               
420                                if( !cell )
421                                        continue;
422                                       
423                                // get the MappingColumn information of the current cell
424                                def mc = mcmap[cell.getColumnIndex()]
425                                def value
426
427                                // Check if column must be imported
428                                if (mc != null && !mc.dontimport && mc.entityclass == entity) {
429                                        try {
430                                                if( cell.getCellType() == Cell.CELL_TYPE_NUMERIC && DateUtil.isCellDateFormatted(cell) ) {
431                                                        // The format for date template fields is dd/mm/yyyy
432                                                        def date = cell.getDateCellValue();
433                                                        value = date.format( "dd/MM/yyyy" )
434                                                } else {
435                                                        value = formatValue(df.formatCellValue(cell), mc.templatefieldtype)
436                                                }
437                                        } catch (NumberFormatException nfe) {
438                                                value = ""
439                                        }
440
441                                        try {
442                                                entityObject.setFieldValue(mc.property, value)
443                                        } catch (Exception iae) {
444                                                log.error ".import wizard error could not set property `" + mc.property + "` to value `" + value + "`"
445
446                                                // store the mapping column and value which failed
447                                                def identifier = entityName.toLowerCase() + "_" + entityObject.getIdentifier() + "_" + mc.property
448
449                                                def mcInstance = new MappingColumn()
450                                                mcInstance.properties = mc.properties
451                                                failed.addToImportcells(new ImportCell(mappingcolumn: mcInstance, value: value, entityidentifier: identifier))
452                                        }
453                                } // end if
454                        } // end for
455
456                        // If a Study is entered, use it as a 'parent' for other entities
457                        if( entity == Study )
458                                parent = entityObject;
459
460                        record << entityObject;
461                }
462
463                // a failed column means that using the entity.setFieldValue() threw an exception
464                return [record, failed]
465        }
466
467        /**
468         * Looks into the database to find an object of the given entity that should be updated, given the excel row.
469         * This is done by looking at the 'preferredIdentifier' field of the object. If it exists in the row, and the
470         * value is already in the database for that field, an existing object is returned. Otherwise, null is returned
471         *
472         * @param       entity          Entity to search
473         * @param       excelRow        Excelrow to search for
474         * @param       mcmap           Map with MappingColumns
475         * @param       parent          Parent study for the entity (if applicable). The returned entity will also have this parent
476         * @param       importedRows    List of entities that have been imported before. The function will first look through this list to find
477         *                                                      a matching entity.
478         * @return      An entity that has the same identifier as entered in the excelRow. The entity is first sought in the importedRows. If it
479         *                      is not found there, the database is queried. If no entity is found at all, null is returned.
480         */
481        def findEntityByRow( Class entity, Row excelRow, def mcmap, Study parent = null, List importedEntities = [], DataFormatter df = null ) {
482                if( !excelRow )
483                        return
484               
485                if( df == null )
486                        df = new DataFormatter();
487
488                def identifierField = givePreferredIdentifier( entity );
489
490                if( identifierField ) {
491                        // Check whether the identifierField is chosen in the column matching
492                        def identifierColumn = mcmap.find { it.entityclass == entity && it.property == identifierField.name };
493
494                        // If it is, find the identifier and look it up in the database
495                        if( identifierColumn ) {
496                                def identifierCell = excelRow.getCell( identifierColumn.index );
497                                def identifier;
498                                try {
499                                        identifier = formatValue(df.formatCellValue(identifierCell), identifierColumn.templatefieldtype)
500                                } catch (NumberFormatException nfe) {
501                                        identifier = null
502                                }
503
504                                // Search for an existing object with the same identifier.
505                                if( identifier ) {
506                                        // First search the already imported rows
507                                        if( importedEntities ) {
508                                                def imported = importedEntities.find { it.getFieldValue( identifierField.name ) == identifier };
509                                                if( imported )
510                                                        return imported;
511                                        }
512
513                                        def c = entity.createCriteria();
514
515                                        // If the entity has a field 'parent', the search should be limited to
516                                        // objects with the same parent. The method entity.hasProperty( "parent" ) doesn't
517                                        // work, since the java.lang.Class entity doesn't know of the parent property.
518                                        if( entity.belongsTo?.containsKey( "parent" ) ) {
519                                                // If the entity requires a parent, but none is given, no
520                                                // results are given from the database. This prevents the user
521                                                // of changing data in another study
522                                                if( parent && parent.id ) {
523                                                        return c.get {
524                                                                eq( identifierField.name, identifier )
525                                                                eq( "parent", parent )
526                                                        }
527                                                }
528                                        } else  {
529                                                return c.get {
530                                                        eq( identifierField.name, identifier )
531                                                }
532                                        }
533                                }
534                        }
535                }
536
537                // No object is found
538                return null;
539        }
540
541        /**
542         * Looks into the list of already imported entities to find an object of the given entity that should be
543         * updated, given the excel row. This is done by looking at the 'preferredIdentifier' field of the object.
544         * If it exists in the row, and the list of imported entities contains an object with the same
545         * identifier, the existing object is returned. Otherwise, null is returned
546         *
547         * @param       entity          Entity to search
548         * @param       excelRow        Excelrow to search for
549         * @param       mcmap           Map with MappingColumns
550         * @param       importedRows    List of entities that have been imported before. The function will first look through this list to find
551         *                                                      a matching entity.
552         * @return      An entity that has the same identifier as entered in the excelRow. The entity is first sought in the importedRows. If it
553         *                      is not found there, the database is queried. If no entity is found at all, null is returned.
554         */
555        def findEntityInImportedEntities( Class entity, Row excelRow, def mcmap, List importedEntities = [], DataFormatter df = null ) {
556                if( df == null )
557                        df = new DataFormatter();
558
559                def allFields = entity.giveDomainFields();
560                def identifierField = allFields.find { it.preferredIdentifier }
561
562                if( identifierField ) {
563                        // Check whether the identifierField is chosen in the column matching
564                        def identifierColumn = mcmap.find { it.entityclass == entity && it.property == identifierField.name };
565
566                        // If it is, find the identifier and look it up in the database
567                        if( identifierColumn ) {
568                                def identifierCell = excelRow.getCell( identifierColumn.index );
569                                def identifier;
570                                try {
571                                        identifier = formatValue(df.formatCellValue(identifierCell), identifierColumn.templatefieldtype)
572                                } catch (NumberFormatException nfe) {
573                                        identifier = null
574                                }
575
576                                // Search for an existing object with the same identifier.
577                                if( identifier ) {
578                                        // First search the already imported rows
579                                        if( importedEntities ) {
580                                                def imported = importedEntities.find {
581                                                        def fieldValue = it.getFieldValue( identifierField.name )
582
583                                                        if( fieldValue instanceof String )
584                                                                return fieldValue.toLowerCase() == identifier.toLowerCase();
585                                                        else
586                                                                return fieldValue == identifier
587                                                };
588                                                if( imported )
589                                                        return imported;
590                                        }
591                                }
592                        }
593                }
594
595                // No object is found
596                return null;
597        }
598
599
600        /**
601         * Creates relation between multiple entities that have been imported. The entities are
602         * all created from one row in the excel sheet.
603         */
604        def relateEntities( List entities) {
605                def study = entities.find { it instanceof Study }
606                def subject = entities.find { it instanceof Subject }
607                def sample = entities.find { it instanceof Sample }
608                def event = entities.find { it instanceof Event }
609                def samplingEvent = entities.find { it instanceof SamplingEvent }
610                def assay = entities.find { it instanceof Assay }
611
612                // A study object is found in the entity list
613                if( study ) {
614                        if( subject ) {
615                                subject.parent = study;
616                                study.addToSubjects( subject );
617                        }
618                        if( sample ) {
619                                sample.parent = study
620                                study.addToSamples( sample );
621                        }
622                        if( event ) {
623                                event.parent = study
624                                study.addToEvents( event );
625                        }
626                        if( samplingEvent ) {
627                                samplingEvent.parent = study
628                                study.addToSamplingEvents( samplingEvent );
629                        }
630                        if( assay ) {
631                                assay.parent = study;
632                                study.addToAssays( assay );
633                        }
634                }
635
636                if( sample ) {
637                        if( subject ) sample.parentSubject = subject
638                        if( samplingEvent ) sample.parentEvent = samplingEvent;
639                        if( event ) {
640                                def evGroup = new EventGroup();
641
642                                sample.parentEventGroup = evGroup;
643                                evGroup.addToEvents( event );
644                               
645                                if( subject ) evGroup.addToSubjects( subject );
646                                if( samplingEvent ) evGroup.addToSamplingEvents( samplingEvent );
647
648                        }
649
650                        if( assay ) assay.addToSamples( sample );
651                }
652        }
653
654        /**
655         * Method to read data from a workbook and to import data into a two dimensional
656         * array
657         *
658         * @param template_id template identifier to use fields from
659         * @param wb POI horrible spreadsheet formatted workbook object
660         * @param mcmap linked hashmap (preserved order) of MappingColumns
661         * @param sheetindex sheet to use when using multiple sheets
662         * @param rowindex first row to start with reading the actual data (NOT the header)
663         * @return two dimensional array containing records (with entities)
664         *
665         * @see dbnp.importer.MappingColumn
666         */
667        def importData(template_id, Workbook wb, int sheetindex, int rowindex, mcmap) {
668                def sheet = wb.getSheetAt(sheetindex)
669                def template = Template.get(template_id)
670                def table = []
671                def failedcells = [] // list of records
672                // walk through all rows and fill the table with records
673                (rowindex..sheet.getLastRowNum()).each { i ->
674                        // Create an entity record based on a row read from Excel and store the cells which failed to be mapped
675                        def (record, failed) = createRecord(template, sheet.getRow(i), mcmap)
676
677                        // Add record with entity and its values to the table
678                        table.add(record)
679
680                        // If failed cells have been found, add them to the failed cells list
681                        if (failed?.importcells?.size() > 0) failedcells.add(failed)
682                }
683
684                return [table, failedcells]
685        }
686
687        /**
688         * Removes a cell from the failedCells list, based on the entity and field. If the entity and field didn't fail before
689         * the method doesn't do anything.
690         *
691         * @param failedcell    list of cells that have failed previously
692         * @param entity                entity to remove from the failedcells list
693         * @param field                 field to remove the failed cell for. If no field is given, all cells for this entity will be removed
694         * @return List                 Updated list of cells that have failed
695         */
696        def removeFailedCell(failedcells, entity, field = null ) {
697                if( !entity )
698                        return failedcells;
699
700                def filterClosure
701                if( field ) {
702                        def entityIdField = "entity_" + entity.getIdentifier() + "_" + field.name.toLowerCase()
703                        filterClosure = { cell -> cell.entityidentifier != entityIdField }
704                } else {
705                        def entityIdField = "entity_" + entity.getIdentifier() + "_"
706                        filterClosure = { cell -> !cell.entityidentifier.startsWith( entityIdField ) }
707                }
708
709                failedcells.each { record ->
710                        record.importcells = record.importcells.findAll( filterClosure )
711                }
712
713                return failedcells;
714        }
715
716        /**
717         * Returns the name of an input field as it is used for a specific entity in HTML.
718         *
719         * @param entity                entity to retrieve the field name for
720         * @param field                 field to retrieve the field name for
721         * @return String               Name of the HTML field for the given entity and field. Can also be used in the map
722         *                                              of request parameters
723         */
724        def getFieldNameInTableEditor(entity, field) {
725                def entityName = entity?.class.name[ entity?.class.name.lastIndexOf(".") + 1..-1]
726
727                if( field instanceof TemplateField )
728                        field = field.escapedName();
729
730                return entityName.toLowerCase() + "_" + entity.getIdentifier() + "_" + field.toLowerCase()
731        }
732
733        /**
734         * Retrieves a mapping column from a list based on the given fieldname
735         * @param mappingColumns                List of mapping columns
736         * @param fieldName                             Field name to find
737         * @return                                              Mapping column if a column is found, null otherwise
738         */
739        def findMappingColumn( mappingColumns, String fieldName ) {
740                return mappingColumns.find { it.property == fieldName.toLowerCase() }
741        }
742
743        /** Method to put failed cells back into the datamatrix. Failed cells are cell values
744         * which could not be stored in an entity (e.g. Humu Supiuns in an ontology field).
745         * Empty corrections should not be stored
746         *
747         * @param datamatrix two dimensional array containing entities and possibly also failed cells
748         * @param failedcells list with maps of failed cells in [mappingcolumn, cell] format
749         * @param correctedcells map of corrected cells in [cellhashcode, value] format
750         * */
751        def saveCorrectedCells(datamatrix, failedcells, correctedcells) {
752
753                // Loop through all failed cells (stored as
754                failedcells.each { record ->
755                        record.value.importcells.each { cell ->
756
757                                // Get the corrected value
758                                def correctedvalue = correctedcells.find { it.key.toInteger() == cell.getIdentifier()}.value
759
760                                // Find the record in the table which the mappingcolumn belongs to
761                                def tablerecord = datamatrix.find { it.hashCode() == record.key }
762
763                                // Loop through all entities in the record and correct them if necessary
764                                tablerecord.each { rec ->
765                                        rec.each { entity ->
766                                                try {
767                                                        // Update the entity field
768                                                        entity.setFieldValue(cell.mappingcolumn.property, correctedvalue)
769                                                        //log.info "Adjusted " + cell.mappingcolumn.property + " to " + correctedvalue
770                                                }
771                                                catch (Exception e) {
772                                                        //log.info "Could not map corrected ontology: " + cell.mappingcolumn.property + " to " + correctedvalue
773                                                }
774                                        }
775                                } // end of table record
776                        } // end of cell record
777                } // end of failedlist
778        }
779
780        /**
781         * Method to store a matrix containing the entities in a record like structure. Every row in the table
782         * contains one or more entity objects (which contain fields with values). So actually a row represents
783         * a record with fields from one or more different entities.
784         *
785         * @param study entity Study
786         * @param datamatrix two dimensional array containing entities with values read from Excel file
787         */
788        static saveDatamatrix(Study study, importerEntityType, datamatrix, authenticationService, log) {
789                def validatedSuccesfully = 0
790                def entitystored = null
791
792                // Study passed? Sync data
793                if (study != null && importerEntityType != 'Study') study.refresh()
794
795                // go through the data matrix, read every record and validate the entity and try to persist it
796                datamatrix.each { record ->
797                        record.each { entity ->
798                                switch (entity.getClass()) {
799                                        case Study: log.info ".importer wizard, persisting Study `" + entity + "`: "
800                                                entity.owner = authenticationService.getLoggedInUser()
801
802                                                if (entity.validate()) {
803                                                        if (!entity.save(flush:true)) {
804                                                                log.error ".importer wizard, study could not be saved: " + entity
805                                                                throw new Exception('.importer wizard, study could not be saved: ' + entity)
806                                                        }
807                                                } else {
808                                                        log.error ".importer wizard, study could not be validated: " + entity
809                                                        throw new Exception('.importer wizard, study could not be validated: ' + entity)
810                                                }
811
812                                                break
813                                        case Subject: log.info ".importer wizard, persisting Subject `" + entity + "`: "
814
815                                        // is the current entity not already in the database?
816                                        //entitystored = isEntityStored(entity)
817
818                                        // this entity is new, so add it to the study
819                                        //if (entitystored==null)
820
821                                                study.addToSubjects(entity)
822
823                                                break
824                                        case Event: log.info ".importer wizard, persisting Event `" + entity + "`: "
825                                                study.addToEvents(entity)
826                                                break
827                                        case Sample: log.info ".importer wizard, persisting Sample `" + entity + "`: "
828
829                                        // is this sample validatable (sample name unique for example?)
830                                                study.addToSamples(entity)
831
832                                                break
833                                        case SamplingEvent: log.info ".importer wizard, persisting SamplingEvent `" + entity + "`: "
834                                                study.addToSamplingEvents(entity)
835                                                break
836                                        default: log.info ".importer wizard, skipping persisting of `" + entity.getclass() + "`"
837                                                break
838                                } // end switch
839                        } // end record
840                } // end datamatrix
841
842                // validate study
843                if (importerEntityType != 'Study') {
844                        if (study.validate()) {
845                                if (!study.save(flush: true)) {
846                                        //this.appendErrors(flow.study, flash.wizardErrors)
847                                        throw new Exception('.importer wizard [saveDatamatrix] error while saving study')
848                                }
849                        } else {
850                                throw new Exception('.importer wizard [saveDatamatrix] study does not validate')
851                        }
852                }
853
854                //persistEntity(study)
855
856                //return [validatedSuccesfully, updatedentities, failedtopersist]
857                //return [0,0,0]
858                return true
859        }
860
861        /**
862         * Check whether an entity already exist. A unique field in the entity is
863         * used to check whether the instantiated entity (read from Excel) is new.
864         * If the entity is found in the database it will be returned as is.
865         *
866         * @param entity entity object like a Study, Subject, Sample et cetera
867         * @return entity if found, otherwise null
868         */
869        def isEntityStored(entity) {
870                switch (entity.getClass()) {
871                        case Study: return Study.findByCode(entity.code)
872                                break
873                        case Subject: return Subject.findByParentAndName(entity.parent, entity.name)
874                                break
875                        case Event: break
876                        case Sample:
877                                break
878                        case SamplingEvent: break
879                        default:  // unknown entity
880                                return null
881                }
882        }
883
884        /**
885         * Find the entity and update the fields. The entity is an instance
886         * read from Excel. This method looks in the database for the entity
887         * having the same identifier. If it has found the same entity
888         * already in the database, it will update the record.
889         *
890         * @param entitystored existing record in the database to update
891         * @param entity entity read from Excel
892         */
893        def updateEntity(entitystored, entity) {
894                switch (entity.getClass()) {
895                        case Study: break
896                        case Subject: entitystored.properties = entity.properties
897                                entitystored.save()
898                                break
899                        case Event: break
900                        case Sample: break
901                        case SamplingEvent: break
902                        default:  // unknown entity
903                                return null
904                }
905        }
906
907        /**
908         * Method to persist entities into the database
909         * Checks whether entity already exists (based on identifier column 'name')
910         *
911         * @param entity entity object like Study, Subject, Protocol et cetera
912         *
913         */
914        boolean persistEntity(entity) {
915                /*log.info ".import wizard persisting ${entity}"
916                 try {           
917                 entity.save(flush: true)
918                 return true
919                 } catch (Exception e) {
920                 def session = sessionFactory.currentSession
921                 session.setFlushMode(org.hibernate.FlushMode.MANUAL)
922                 log.error ".import wizard, failed to save entity:\n" + org.apache.commons.lang.exception.ExceptionUtils.getRootCauseMessage(e)
923                 }
924                 return true*/
925                //println "persistEntity"
926        }
927
928        /**
929         * This method creates a record (array) containing entities with values
930         *
931         * @param template_id template identifier
932         * @param excelrow POI based Excel row containing the cells
933         * @param mcmap map containing MappingColumn objects
934         * @return list of entities and list of failed cells
935         */
936        def createRecord(template, Row excelrow, mcmap) {
937                def df = new DataFormatter()
938                def tft = TemplateFieldType
939                def record = [] // list of entities and the read values
940                def failed = new ImportRecord() // map with entity identifier and failed mappingcolumn
941
942                // Initialize all possible entities with the chosen template
943                def study = new Study(template: template)
944                def subject = new Subject(template: template)
945                def samplingEvent = new SamplingEvent(template: template)
946                def event = new Event(template: template)
947                def sample = new Sample(template: template)
948
949                // Go through the Excel row cell by cell
950                for (Cell cell: excelrow) {
951                        // get the MappingColumn information of the current cell
952                        def mc = mcmap[cell.getColumnIndex()]
953                        def value
954
955                        // Check if column must be imported
956                        if (mc != null) if (!mc.dontimport) {
957                                try {
958                                        value = formatValue(df.formatCellValue(cell), mc.templatefieldtype)
959                                } catch (NumberFormatException nfe) {
960                                        value = ""
961                                }
962
963                                try {
964                                        // which entity does the current cell (field) belong to?
965                                        switch (mc.entityclass) {
966                                                case Study: // does the entity already exist in the record? If not make it so.
967                                                        (record.any {it.getClass() == mc.entityclass}) ? 0 : record.add(study)
968                                                        study.setFieldValue(mc.property, value)
969                                                        break
970                                                case Subject: (record.any {it.getClass() == mc.entityclass}) ? 0 : record.add(subject)
971                                                        subject.setFieldValue(mc.property, value)
972                                                        break
973                                                case SamplingEvent: (record.any {it.getClass() == mc.entityclass}) ? 0 : record.add(samplingEvent)
974                                                        samplingEvent.setFieldValue(mc.property, value)
975                                                        break
976                                                case Event: (record.any {it.getClass() == mc.entityclass}) ? 0 : record.add(event)
977                                                        event.setFieldValue(mc.property, value)
978                                                        break
979                                                case Sample: (record.any {it.getClass() == mc.entityclass}) ? 0 : record.add(sample)
980                                                        sample.setFieldValue(mc.property, value)
981                                                        break
982                                                case Object:   // don't import
983                                                        break
984                                        } // end switch
985                                } catch (Exception iae) {
986                                        log.error ".import wizard error could not set property `" + mc.property + "` to value `" + value + "`"
987                                        // store the mapping column and value which failed
988                                        def identifier
989                                        def fieldName = mc.property?.toLowerCase()
990                                       
991                                        switch (mc.entityclass) {
992                                                case Study: identifier = "entity_" + study.getIdentifier() + "_" + fieldName
993                                                        break
994                                                case Subject: identifier = "entity_" + subject.getIdentifier() + "_" + fieldName
995                                                        break
996                                                case SamplingEvent: identifier = "entity_" + samplingEvent.getIdentifier() + "_" + fieldName
997                                                        break
998                                                case Event: identifier = "entity_" + event.getIdentifier() + "_" + fieldName
999                                                        break
1000                                                case Sample: identifier = "entity_" + sample.getIdentifier() + "_" + fieldName
1001                                                        break
1002                                                case Object:   // don't import
1003                                                        break
1004                                        }
1005
1006                                        def mcInstance = new MappingColumn()
1007                                        mcInstance.properties = mc.properties
1008                                        failed.addToImportcells(new ImportCell(mappingcolumn: mcInstance, value: value, entityidentifier: identifier))
1009                                }
1010                        } // end
1011                } // end for
1012                // a failed column means that using the entity.setFieldValue() threw an exception
1013                return [record, failed]
1014        }
1015
1016        /**
1017         * Method to parse a value conform a specific type
1018         * @param value string containing the value
1019         * @return object corresponding to the TemplateFieldType
1020         */
1021        def formatValue(String value, TemplateFieldType type) throws NumberFormatException {
1022                switch (type) {
1023                        case TemplateFieldType.STRING: return value.trim()
1024                        case TemplateFieldType.TEXT: return value.trim()
1025                        case TemplateFieldType.LONG: return (long) Double.valueOf(value)
1026                        //case TemplateFieldType.FLOAT      :   return Float.valueOf(value.replace(",","."));
1027                        case TemplateFieldType.DOUBLE: return Double.valueOf(value.replace(",", "."));
1028                        case TemplateFieldType.STRINGLIST: return value.trim()
1029                        case TemplateFieldType.ONTOLOGYTERM: return value.trim()
1030                        case TemplateFieldType.DATE: return value
1031                        default: return value
1032                }
1033        }
1034
1035        /**
1036         * Returns the preferred identifier field for a given entity or
1037         * null if no preferred identifier is given
1038         * @param entity        TemplateEntity class
1039         * @return      The preferred identifier field or NULL if no preferred identifier is given
1040         */
1041        public TemplateField givePreferredIdentifier( Class entity ) {
1042                def allFields = entity.giveDomainFields();
1043                return allFields.find { it.preferredIdentifier }
1044        }
1045
1046        // classes for fuzzy string matching
1047        // <FUZZY MATCHING>
1048
1049        static def similarity(l_seq, r_seq, degree = 2) {
1050                def l_histo = countNgramFrequency(l_seq, degree)
1051                def r_histo = countNgramFrequency(r_seq, degree)
1052
1053                dotProduct(l_histo, r_histo) /
1054                                Math.sqrt(dotProduct(l_histo, l_histo) *
1055                                dotProduct(r_histo, r_histo))
1056        }
1057
1058        static def countNgramFrequency(sequence, degree) {
1059                def histo = [:]
1060                def items = sequence.size()
1061
1062                for (int i = 0; i + degree <= items; i++) {
1063                        def gram = sequence[i..<(i + degree)]
1064                        histo[gram] = 1 + histo.get(gram, 0)
1065                }
1066                histo
1067        }
1068
1069        static def dotProduct(l_histo, r_histo) {
1070                def sum = 0
1071                l_histo.each { key, value ->
1072                        sum = sum + l_histo[key] * r_histo.get(key, 0)
1073                }
1074                sum
1075        }
1076
1077        static def stringSimilarity(l_str, r_str, degree = 2) {
1078
1079                similarity(l_str.toString().toLowerCase().toCharArray(),
1080                                r_str.toString().toLowerCase().toCharArray(),
1081                                degree)
1082        }
1083
1084        static def mostSimilar(pattern, candidates, threshold = 0) {
1085                def topScore = 0
1086                def bestFit = null
1087
1088                candidates.each { candidate ->
1089                        def score = stringSimilarity(pattern, candidate)
1090                        if (score > topScore) {
1091                                topScore = score
1092                                bestFit = candidate
1093                        }
1094                }
1095
1096                if (topScore < threshold)
1097                        bestFit = null
1098
1099                bestFit
1100        }
1101        // </FUZZY MATCHING>
1102
1103}
Note: See TracBrowser for help on using the repository browser.