Ignore:
Timestamp:
Mar 7, 2011, 12:01:52 PM (11 years ago)
Author:
robert@…
Message:

Improved simple wizard so that is can edit studies

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/grails-app/services/dbnp/importer/ImporterService.groovy

    r1588 r1591  
    1515 */
    1616package dbnp.importer
     17
    1718import org.dbnp.gdt.*
    1819import org.apache.poi.ss.usermodel.*
     
    186187        }
    187188
     189       
     190        /**
     191         * Retrieves records with sample, subject, samplingevent etc. from a study
     192         * @param s             Study to retrieve records from
     193         * @return              A list with hashmaps [ 'objects': [ 'Sample': .., 'Subject': .., 'SamplingEvent': .., 'Event': '.. ], 'templates': [], 'templateCombination': .. ]
     194         */
     195        protected def getRecords( Study s ) {
     196                def records = [];
     197               
     198                s.samples?.each {
     199                        def record = [ 'objects': retrieveEntitiesBySample( it ) ];
     200               
     201                        def templates = [:]
     202                        def templateCombination = [];
     203                        record.objects.each { entity ->
     204                                templates[ entity.key ] = entity.value?.template
     205                                if( entity.value?.template )
     206                                        templateCombination << entity.key + ": " + entity.value?.template?.name;
     207                        }
     208                       
     209                        record.templates = templates;
     210                        record.templateCombination = templateCombination.join( ', ' )
     211                       
     212                        records << record
     213                }
     214               
     215                return records;
     216        }
     217       
     218        /**
     219         * Returns a subject, event and samplingEvent that belong to this sample
     220         * @param s             Sample to find the information for
     221         * @return
     222         */
     223        protected retrieveEntitiesBySample( Sample s ) {
     224                return [
     225                        'Sample': s,
     226                        'Subject': s?.parentSubject,
     227                        'SamplingEvent': s?.parentEvent,
     228                        'Event': s?.parentEventGroup?.events?.getAt(0)
     229                ]
     230        }
     231       
     232        /**
     233         * Imports data from a workbook into a list of ImportRecords. If some entities are already in the database,
     234         * these records are updated.
     235         *
     236         * This method is capable of importing Subject, Samples, SamplingEvents and Events
     237         *
     238         * @param       templates       Map of templates, identified by their entity as a key. For example: [ Subject: Template x, Sample: Template y ]
     239         * @param       wb                      Excel workbook to import
     240         * @param       sheetindex      Number of the sheet to import data from
     241         * @param       rowindex        Row to start importing from.
     242         * @param       mcmap           Hashmap of mappingcolumns, with the first entry in the hashmap containing information about the first column, etc.
     243         * @param       parent          Study to import all data into. Is used for determining which sample/event/subject/assay to update
     244         * @param       createAllEntities       If set to true, the system will also create objects for entities that have no data imported, but do have
     245         *                                                              a template assigned
     246         * @return      List            List with two entries:
     247         *                      0                       List with ImportRecords, one for each row in the excelsheet
     248         *                      1                       List with ImportCell objects, mentioning the cells that could not be correctly imported
     249         *                                              (because the value in the excelsheet can't be entered into the template field)
     250         */
     251        def importOrUpdateDataBySampleIdentifier( def templates, Workbook wb, int sheetindex, int rowindex, def mcmap, Study parent = null, boolean createAllEntities = true ) {
     252                if( !mcmap )
     253                        return;
     254                       
     255                // Check whether the rows should be imported in one or more entities
     256                def entities
     257                if( createAllEntities ) {
     258                        entities = templates.entrySet().value.findAll { it }.entity;
     259                } else {
     260                        entities = mcmap.findAll{ !it.dontimport }.entityclass.unique();
     261                }
     262               
     263                def sheet = wb.getSheetAt(sheetindex)
     264                def table = []
     265                def failedcells = [] // list of cells that have failed to import
     266
     267                // First check for each record whether an entity in the database should be updated,
     268                // or a new entity should be added. This is done before any new object is created, since
     269                // searching after new objects have been created (but not yet saved) will result in
     270                //      org.hibernate.AssertionFailure: collection [...] was not processed by flush()
     271                // errors
     272                def existingEntities = [:]
     273                for( int i = rowindex; i <= sheet.getLastRowNum(); i++ ) {
     274                        existingEntities[i] = findExistingEntities( entities, sheet.getRow(i), mcmap, parent );
     275                }
     276                               
     277                // walk through all rows and fill the table with records
     278                for( int i = rowindex; i <= sheet.getLastRowNum(); i++ ) {
     279                        // Create an entity record based on a row read from Excel and store the cells which failed to be mapped
     280                        def (record, failed) = importOrUpdateRecord( templates, entities, sheet.getRow(i), mcmap, parent, table, existingEntities[i] );
     281                       
     282                        // Setup the relationships between the imported entities
     283                        relateEntities( record );
     284                       
     285                        // Add record with entities and its values to the table
     286                        table.add(record)
     287
     288                        // If failed cells have been found, add them to the failed cells list
     289                        if (failed?.importcells?.size() > 0) failedcells.add(failed)
     290                }
     291               
     292                return [ "table": table, "failedCells": failedcells ]
     293        }
     294
     295        /**
     296        * Checks whether entities in the given row already exist in the database
     297        * they are updated.
     298        *
     299        * @param        entities        Entities that have to be imported for this row
     300        * @param        excelRow        Excel row to import into this record
     301        * @param        mcmap           Hashmap of mappingcolumns, with the first entry in the hashmap containing information about the first column, etc.
     302        * @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>]
     303        */
     304   def findExistingEntities(def entities, Row excelRow, mcmap, parent ) {
     305           DataFormatter df = new DataFormatter();
     306
     307           // Find entities based on sample identifier
     308           def sample = findEntityByRow( dbnp.studycapturing.Sample, excelRow, mcmap, parent, [], df );
     309           return retrieveEntitiesBySample( sample );
     310   }
     311   
     312        /**
     313         * Imports a records from the excelsheet into the database. If the entities are already in the database
     314         * they are updated.
     315         *
     316         * This method is capable of importing Subject, Samples, SamplingEvents and Events
     317         *
     318         * @param       templates       Map of templates, identified by their entity as a key. For example: [ Sample: Template y ]
     319         * @param       entities        Entities that have to be imported for this row
     320         * @param       excelRow        Excel row to import into this record
     321         * @param       mcmap           Hashmap of mappingcolumns, with the first entry in the hashmap containing information about the first column, etc.
     322         * @param       parent          Study to import all data into. Is used for determining which sample/event/subject/assay to update
     323         * @param       importedRows    Rows that have been imported before this row. These rows might contain the same entities as are
     324         *                                                      imported in this row. These entities should be used again, to avoid importing duplicates.
     325         * @return      List            List with two entries:
     326         *                      0                       List with ImportRecords, one for each row in the excelsheet
     327         *                      1                       List with ImportCell objects, mentioning the cells that could not be correctly imported
     328         *                                              (because the value in the excelsheet can't be entered into the template field)
     329         */
     330        def importOrUpdateRecord(def templates, def entities, Row excelRow, mcmap, Study parent = null, List importedRows, Map existingEntities ) {
     331                DataFormatter df = new DataFormatter();
     332                def record = [] // list of entities and the read values
     333                def failed = new ImportRecord() // map with entity identifier and failed mappingcolumn
     334       
     335                // Check whether this record mentions a sample that has been imported before. In that case,
     336                // we update that record, in order to prevent importing the same sample multiple times
     337                def importedEntities = [];
     338                if( importedRows )
     339                        importedEntities = importedRows.flatten().findAll { it.class == dbnp.studycapturing.Sample }.unique();
     340
     341                def importedSample = findEntityInImportedEntities( dbnp.studycapturing.Sample, excelRow, mcmap, importedEntities, df )
     342                def imported = retrieveEntitiesBySample( importedSample );
     343               
     344                for( entity in entities ) {
     345                        // Check whether this entity should be added or updated
     346                        // The entity is updated is an entity with the same 'identifier' (field
     347                        // specified to be the identifying field) is found in the database
     348                        def entityName = entity.name[ entity.name.lastIndexOf( '.' ) + 1..-1];
     349                        def template = templates[ entityName ];
     350                       
     351                        // If no template is specified for this entity, continue with the next
     352                        if( !template )
     353                                continue;
     354                       
     355                        // Check whether the object exists in the list of already imported entities
     356                        def entityObject = imported[ entityName ]
     357                       
     358                        // If it doesn't, search for the entity in the database
     359                        if( !entityObject && existingEntities )
     360                                entityObject = existingEntities[ entityName ];
     361                       
     362                        // Otherwise, create a new object
     363                        if( !entityObject )
     364                                entityObject = entity.newInstance();
     365                       
     366                        // Update the template
     367                        entityObject.template = template;
     368                       
     369                        // Go through the Excel row cell by cell
     370                        for (Cell cell: excelRow) {
     371                                // get the MappingColumn information of the current cell
     372                                def mc = mcmap[cell.getColumnIndex()]
     373                                def value
     374         
     375                                // Check if column must be imported
     376                                if (mc != null && !mc.dontimport && mc.entityclass == entity) {
     377                                        try {
     378                                                value = formatValue(df.formatCellValue(cell), mc.templatefieldtype)
     379                                        } catch (NumberFormatException nfe) {
     380                                                value = ""
     381                                        }
     382         
     383                                        try {
     384                                                entityObject.setFieldValue(mc.property, value)
     385                                        } catch (Exception iae) {
     386                                                log.error ".import wizard error could not set property `" + mc.property + "` to value `" + value + "`"
     387
     388                                                // store the mapping column and value which failed
     389                                                def identifier = entityName.toLowerCase() + "_" + entityObject.getIdentifier() + "_" + mc.property
     390         
     391                                                def mcInstance = new MappingColumn()
     392                                                mcInstance.properties = mc.properties
     393                                                failed.addToImportcells(new ImportCell(mappingcolumn: mcInstance, value: value, entityidentifier: identifier))
     394                                        }
     395                                } // end if
     396                        } // end for
     397                       
     398                        // If a Study is entered, use it as a 'parent' for other entities
     399                        if( entity == Study )
     400                                parent = entityObject;
     401                       
     402                        record << entityObject;
     403                }
     404               
     405                // a failed column means that using the entity.setFieldValue() threw an exception
     406                return [record, failed]
     407        }
     408       
     409        /**
     410         * Looks into the database to find an object of the given entity that should be updated, given the excel row.
     411         * This is done by looking at the 'preferredIdentifier' field of the object. If it exists in the row, and the
     412         * value is already in the database for that field, an existing object is returned. Otherwise, null is returned
     413         *
     414         * @param       entity          Entity to search
     415         * @param       excelRow        Excelrow to search for
     416         * @param       mcmap           Map with MappingColumns
     417         * @param       parent          Parent study for the entity (if applicable). The returned entity will also have this parent
     418         * @param       importedRows    List of entities that have been imported before. The function will first look through this list to find
     419         *                                                      a matching entity.
     420         * @return      An entity that has the same identifier as entered in the excelRow. The entity is first sought in the importedRows. If it
     421         *                      is not found there, the database is queried. If no entity is found at all, null is returned.
     422         */
     423        def findEntityByRow( Class entity, Row excelRow, def mcmap, Study parent = null, List importedEntities = [], DataFormatter df = null ) {
     424                if( df == null )
     425                        df = new DataFormatter();
     426                       
     427                def identifierField = givePreferredIdentifier( entity );
     428               
     429                if( identifierField ) {
     430                        // Check whether the identifierField is chosen in the column matching
     431                        def identifierColumn = mcmap.find { it.entityclass == entity && it.property == identifierField.name };
     432                       
     433                        // If it is, find the identifier and look it up in the database
     434                        if( identifierColumn ) {
     435                                def identifierCell = excelRow.getCell( identifierColumn.index );
     436                                def identifier;
     437                                try {
     438                                        identifier = formatValue(df.formatCellValue(identifierCell), identifierColumn.templatefieldtype)
     439                                } catch (NumberFormatException nfe) {
     440                                        identifier = null
     441                                }
     442                               
     443                                // Search for an existing object with the same identifier.
     444                                if( identifier ) {
     445                                        // First search the already imported rows
     446                                        if( importedEntities ) {
     447                                                def imported = importedEntities.find { it.getFieldValue( identifierField.name ) == identifier };
     448                                                if( imported )
     449                                                        return imported;
     450                                        }
     451                                       
     452                                        def c = entity.createCriteria();
     453                                       
     454                                        // If the entity has a field 'parent', the search should be limited to
     455                                        // objects with the same parent. The method entity.hasProperty( "parent" ) doesn't
     456                                        // work, since the java.lang.Class entity doesn't know of the parent property.
     457                                        if( entity.belongsTo?.containsKey( "parent" ) ) {
     458                                                // If the entity requires a parent, but none is given, no
     459                                                // results are given from the database. This prevents the user
     460                                                // of changing data in another study
     461                                                if( parent && parent.id ) {
     462                                                        println "Searching (with parent ) for " + entity.name + " with " + identifierField.name + " = " + identifier
     463                                                        return c.get {
     464                                                                eq( identifierField.name, identifier )
     465                                                                eq( "parent", parent )
     466                                                        }
     467                                                }
     468                                        } else  {
     469                                                println "Searching (without parent ) for " + entity.name + " with " + identifierField.name + " = " + identifier
     470                                                return c.get {
     471                                                        eq( identifierField.name, identifier )
     472                                                }
     473                                        }
     474                                }
     475                        }
     476                }
     477               
     478                // No object is found
     479                return null;
     480        }
     481       
     482        /**
     483        * Looks into the list of already imported entities to find an object of the given entity that should be
     484        * updated, given the excel row. This is done by looking at the 'preferredIdentifier' field of the object.
     485        * If it exists in the row, and the list of imported entities contains an object with the same
     486        * identifier, the existing object is returned. Otherwise, null is returned
     487        *
     488        * @param        entity          Entity to search
     489        * @param        excelRow        Excelrow to search for
     490        * @param        mcmap           Map with MappingColumns
     491        * @param        importedRows    List of entities that have been imported before. The function will first look through this list to find
     492        *                                                       a matching entity.
     493        * @return       An entity that has the same identifier as entered in the excelRow. The entity is first sought in the importedRows. If it
     494        *                       is not found there, the database is queried. If no entity is found at all, null is returned.
     495        */
     496   def findEntityInImportedEntities( Class entity, Row excelRow, def mcmap, List importedEntities = [], DataFormatter df = null ) {
     497           if( df == null )
     498                   df = new DataFormatter();
     499                   
     500           def allFields = entity.giveDomainFields();
     501           def identifierField = allFields.find { it.preferredIdentifier }
     502           
     503           if( identifierField ) {
     504                   // Check whether the identifierField is chosen in the column matching
     505                   def identifierColumn = mcmap.find { it.entityclass == entity && it.property == identifierField.name };
     506                   
     507                   // If it is, find the identifier and look it up in the database
     508                   if( identifierColumn ) {
     509                           def identifierCell = excelRow.getCell( identifierColumn.index );
     510                           def identifier;
     511                           try {
     512                                   identifier = formatValue(df.formatCellValue(identifierCell), identifierColumn.templatefieldtype)
     513                           } catch (NumberFormatException nfe) {
     514                                   identifier = null
     515                           }
     516                           
     517                           // Search for an existing object with the same identifier.
     518                           if( identifier ) {
     519                                        // First search the already imported rows
     520                                        if( importedEntities ) {
     521                                                def imported = importedEntities.find {
     522                                                        def fieldValue = it.getFieldValue( identifierField.name )
     523
     524                                                        if( fieldValue instanceof String )
     525                                                                return fieldValue.toLowerCase() == identifier.toLowerCase();
     526                                                        else
     527                                                                return fieldValue == identifier
     528
     529                                                };
     530                                           if( imported )
     531                                                   return imported;
     532                                   }
     533                           }
     534                   }
     535           }
     536           
     537           // No object is found
     538           return null;
     539   }
     540
     541       
     542        /**
     543         * Creates relation between multiple entities that have been imported. The entities are
     544         * all created from one row in the excel sheet.
     545         */
     546        def relateEntities( List entities) {
     547                def study = entities.find { it instanceof Study }
     548                def subject = entities.find { it instanceof Subject }
     549                def sample = entities.find { it instanceof Sample }
     550                def event = entities.find { it instanceof Event }
     551                def samplingEvent = entities.find { it instanceof SamplingEvent }
     552                def assay = entities.find { it instanceof Assay }
     553               
     554                // A study object is found in the entity list
     555                if( study ) {
     556                        if( subject ) {
     557                                subject.parent = study;
     558                                study.addToSubjects( subject );
     559                        }
     560                        if( sample ) {
     561                                sample.parent = study
     562                                study.addToSamples( sample );
     563                        }
     564                        if( event ) {
     565                                event.parent = study
     566                                study.addToEvents( event );
     567                        }
     568                        if( samplingEvent ) {
     569                                samplingEvent.parent = study
     570                                study.addToSamplingEvents( samplingEvent );
     571                        }
     572                        if( assay ) {
     573                                assay.parent = study;
     574                                study.addToAssays( assay );
     575                        }
     576                }
     577
     578                if( sample ) {
     579                        if( subject ) sample.parentSubject = subject
     580                        if( samplingEvent ) sample.parentEvent = samplingEvent;
     581                        if( event ) {
     582                                def evGroup = new EventGroup();
     583                                evGroup.addToEvents( event );
     584                                if( subject ) evGroup.addToSubjects( subject );
     585                                if( samplingEvent ) evGroup.addToSamplingEvents( samplingEvent );
     586                               
     587                                sample.parentEventGroup = evGroup;
     588                        }
     589                       
     590                        if( assay ) assay.addToSamples( sample );
     591                }
     592        }
     593
    188594        /**
    189595         * Method to read data from a workbook and to import data into a two dimensional
     
    258664         */
    259665        def getFieldNameInTableEditor(entity, field) {
     666                def entityName = entity?.class.name[ entity?.class.name.lastIndexOf(".") + 1..-1]
     667               
    260668                if( field instanceof TemplateField )
    261669                        field = field.escapedName();
    262670
    263                 return "entity_" + entity.getIdentifier() + "_" + field
     671                return entityName.toLowerCase() + "_" + entity.getIdentifier() + "_" + field
    264672        }
    265673
     
    562970                }
    563971        }
     972       
     973        /**
     974         * Returns the preferred identifier field for a given entity or
     975         * null if no preferred identifier is given
     976         * @param entity        TemplateEntity class
     977         * @return      The preferred identifier field or NULL if no preferred identifier is given
     978         */
     979        public TemplateField givePreferredIdentifier( Class entity ) {
     980                def allFields = entity.giveDomainFields();
     981                return allFields.find { it.preferredIdentifier }
     982        }
    564983
    565984        // classes for fuzzy string matching
Note: See TracChangeset for help on using the changeset viewer.