source: trunk/grails-app/controllers/dbnp/importer/ImporterController.groovy @ 1515

Last change on this file since 1515 was 1515, checked in by t.w.abma@…, 10 years ago
  • added load/save button to mappings page
  • fuzzy button applies fuzzy matching
  • fixed bug when pressing save didn't remember selected properties and went into "fuzzy mode"
  • importmapping instance is stored, but adding the mappingcolumns is the last "todo"
  • Property svn:keywords set to Rev Author Date
File size: 21.0 KB
Line 
1package dbnp.importer
2
3import dbnp.studycapturing.*
4import org.dbnp.gdt.*
5import grails.converters.JSON
6import org.codehaus.groovy.grails.plugins.web.taglib.ValidationTagLib
7import grails.plugins.springsecurity.Secured
8
9/**
10 * Wizard Controller
11 *
12 * @author Jeroen Wesbeek
13 * @since 20101206
14 *
15 * Revision information:
16 * $Rev: 1515 $
17 * $Author: t.w.abma@umcutrecht.nl $
18 * $Date: 2011-02-11 13:59:27 +0000 (vr, 11 feb 2011) $
19 */
20@Secured(['IS_AUTHENTICATED_REMEMBERED'])
21class ImporterController {
22        // the pluginManager is used to check if the Grom
23        // plugin is available so we can 'Grom' development
24        // notifications to the unified notifications daemon
25        // (see http://www.grails.org/plugin/grom)
26        def pluginManager
27        def authenticationService
28        def fileService
29        def ImporterService
30        def validationTagLib = new ValidationTagLib()
31        def GdtService
32
33        /**
34         * index method, redirect to the webflow
35         * @void
36         */
37        def index = {
38                // Grom a development message
39                if (pluginManager.getGrailsPlugin('grom')) "redirecting into the webflow".grom()
40
41                /**
42                 * Do you believe it in your head?
43                 * I can go with the flow
44                 * Don't say it doesn't matter (with the flow) matter anymore
45                 * I can go with the flow (I can go)
46                 * Do you believe it in your head?
47                 */
48                redirect(action: 'pages')
49        }
50
51        /**
52         * WebFlow definition
53         * @void
54         */
55        def pagesFlow = {
56                // start the flow
57                onStart {
58                        // Grom a development message
59                        if (pluginManager.getGrailsPlugin('grom')) "entering the WebFlow".grom()
60
61                        // define variables in the flow scope which is availabe
62                        // throughout the complete webflow also have a look at
63                        // the Flow Scopes section on http://www.grails.org/WebFlow
64                        //
65                        // The following flow scope variables are used to generate
66                        // wizard tabs. Also see common/_tabs.gsp for more information
67                        flow.page = 0
68                        flow.pages = [
69                                [title: 'Import file'],
70                                [title: 'Assign properties'],
71                                [title: 'Check imported data'],
72                                //[title: 'Imported'],
73                                [title: 'Done']
74                        ]
75                        flow.cancel = true;
76                        flow.quickSave = true;
77
78                        success()
79                }
80
81                // render the main wizard page which immediately
82                // triggers the 'next' action (hence, the main
83                // page dynamically renders the study template
84                // and makes the flow jump to the study logic)
85                mainPage {
86                        render(view: "/importer/index")
87                        onRender {
88                                // Grom a development message
89                                if (pluginManager.getGrailsPlugin('grom')) "rendering the main Ajaxflow page (index.gsp)".grom()
90
91                                // let the view know we're in page 1
92                                flow.page = 1
93                                success()
94                        }
95                        on("next").to "pageOne"
96                }
97
98                // File import and entitie template selection page
99                pageOne {
100                        render(view: "_page_one")
101                        onRender {
102                                log.info ".entering import wizard"
103                                // Grom a development message
104                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial: pages/_page_one.gsp".grom()
105
106                                flow.page = 1
107                                flow.studies = Study.findAllWhere(owner: authenticationService.getLoggedInUser())
108                flow.importer_fuzzymatching="false"
109
110                                success()
111                        }
112
113            on("refresh") {
114                flash.importer_params = params           
115                                success()
116                        }.to "pageOne"
117
118                        on("next") {
119                                flash.wizardErrors = [:]
120                //flash.importer_params = params
121
122                                // Study selected?
123                                flow.importer_study = (params.study) ? Study.get(params.study.id.toInteger()) : null
124
125                                // Trying to import data into an existing study?
126                                if (flow.importer_study)
127                                        if (flow.importer_study.canWrite(authenticationService.getLoggedInUser())) 
128                                                fileImportPage(flow, flash, params) ? success() : error()                                       
129                                        else {
130                                                log.error ".importer wizard wrong permissions"
131                                                this.appendErrorMap(['error': "You don't have the right permissions"], flash.wizardErrors)
132                                                error()
133                                        }
134                                else {
135                                        fileImportPage(flow, flash, params) ? success() : error()
136                                }
137
138                                // put your bussiness logic (if applicable) in here
139                        }.to "pageTwo"
140                }
141
142                // Property to column assignment page
143                pageTwo {
144                        render(view: "_page_two")
145                        onRender {
146                                log.info ".import wizard properties page"
147                                // Grom a development message
148                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial: pages/_page_two.gsp".grom()
149
150                flow.importer_importmappings = ImportMapping.list()
151
152                                flow.page = 2
153                                success()
154                        }
155            on("refresh") {
156                flow.importer_importmappings = ImportMapping.list()
157
158                // a name was given to the current property mapping, try to store it
159                if (params.mappingname) {
160                    flash.importer_columnproperty = params.columnproperty
161                    propertiesSaveImportMappingPage(flow, flash, params)                   
162                }
163               
164                if (params.fuzzymatching == "true")
165                    flow.importer_fuzzymatching="true" else
166                    flow.importer_fuzzymatching="false"
167
168                success()
169                        }.to "pageTwo"
170
171                        on("next") {
172                                if (propertiesPage(flow, flash, params)) {
173                                        success()
174                                } else {
175                                        log.error ".import wizard, properties are set wrong"
176                                        error()
177                                }
178                        }.to "pageThree"
179                        on("previous").to "pageOne"
180                }
181
182                // Mapping page
183                pageThree {
184                        render(view: "_page_three")
185                        onRender {
186                                log.info ".import wizard mapping page"
187                                // Grom a development message
188                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_page_three.gsp".grom()
189
190                                flow.page = 3
191                                success()
192                        }
193                        on("refresh") {
194                                success()
195                        }.to "pageThree"
196                        on("next") {
197                                if (mappingsPage(flow, flash, params)) {
198                                        flow.page = 4
199                                        success()
200                                } else {
201                                        log.error ".import wizard mapping error, could not validate all entities"
202                                        error()
203                                }
204                        }.to "save"
205                        on("previous").to "pageTwo"
206                }
207
208                // Imported data overview page
209                pageFour {
210                        render(view: "_page_four")
211                        onRender {
212                                // Grom a development message
213                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_page_four.gsp".grom()
214
215                                flow.page = 4
216                                success()
217                        }
218                        on("next") {
219                                if (importedPage(flow, params)) {
220                                        flow.page = 4
221                                        success()
222                                } else {
223                                        log.error ".import wizard imported error, something went wrong showing the imported entities"
224                                        error()
225                                }
226                        }.to "save"
227                        on("previous").to "pageThree"
228                }
229
230                // Save the imported data
231                save {
232                        action {
233                                // here you can validate and save the
234                                // instances you have created in the
235                                // ajax flow.
236                                // Grom a development message
237                                if (pluginManager.getGrailsPlugin('grom')) ".persisting instances to the database...".grom()
238
239                                if (saveEntities(flow, params)) {
240                                        //if (ImporterService.saveDatamatrix(flow.importer_study, flow.importer_importeddata)) {
241                                        //log.error ".import wizard succesfully persisted all entities"
242                                        //def session = sessionFactory.getCurrentSession()
243                                        //session.clear()
244                                        success()
245                                } else {
246                                        log.error ".import wizard, could not save entities:\n" + e.dump()
247                                        flow.page = 4
248                                        error()
249                                }
250                        }
251                        on("error").to "error"
252                        on(Exception).to "error"
253                        on("success").to "finalPage"
254                }
255
256                // render errors
257                error {
258                        render(view: "_error")
259                        onRender {
260                                // Grom a development message
261                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_error.gsp".grom()
262
263                                // set page to 4 so that the navigation
264                                // works (it is disabled on the final page)
265                                flow.page = 4
266                        }
267                        on("next").to "save"
268                        on("previous").to "pageFour"
269                }
270
271                // last wizard page
272                finalPage {
273                        render(view: "_final_page")
274                        onRender {
275                                // Grom a development message
276                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_final_page.gsp".grom()
277                                success()
278                        }
279                        onEnd {
280                                // clean flow scope
281                                flow.clear()
282                        }
283                }
284        }
285
286    def propertiesManager = {
287        render (view:"common/_propertiesManager")
288    }
289
290        /**
291         * Return templates which belong to a certain entity type
292         *
293         * @param entity entity name string (Sample, Subject, Study et cetera)
294         * @return JSON object containing the found templates
295         */
296        def ajaxGetTemplatesByEntity = {
297                // fetch all templates for a specific entity
298                def templates = Template.findAllByEntity(GdtService.getInstanceByEntity(params.entity.decodeURL()))
299
300
301                // render as JSON
302                render templates as JSON
303        }
304
305        /**
306         * Handle the file import page.
307         *
308         * @param Map LocalAttributeMap (the flow scope)
309         * @param Map GrailsParameterMap (the flow parameters = form data)
310         * @returns boolean true if correctly validated, otherwise false
311         */
312        boolean fileImportPage(flow, flash, params) {
313                def importedfile = fileService.get(params['importfile'])
314                //fileService.delete(YourFile)
315
316        if (importedfile.exists()) {
317            try {
318                session.importer_workbook = ImporterService.getWorkbook(new FileInputStream(importedfile))
319            } catch (Exception e) {
320                log.error ".importer wizard could not load file: " + e
321                this.appendErrorMap(['error': "Wrong file (format), the importer requires an Excel file as input"], flash.wizardErrors)
322                return false
323            }
324        }
325
326                if (params.entity && params.template_id) {           
327                       
328            try {
329                session.importer_workbook = ImporterService.getWorkbook(new FileInputStream(importedfile))
330            } catch (Exception e) {
331                log.error ".importer wizard could not load file: " + e
332                this.appendErrorMap(['error': "Excel file required as input"], flash.wizardErrors)
333                return false
334            }
335
336                        def selectedentities = []
337
338                        def entityName = GdtService.decryptEntity(params.entity.decodeURL())
339                        def entityClass = GdtService.getInstanceByEntityName(entityName)
340
341                        // Initialize some session variables
342                        //flow.importer_workbook = wb // workbook object must be serialized for this to work
343
344                        flow.importer_template_id = params.template_id
345                        flow.importer_sheetindex = params.sheetindex.toInteger() - 1 // 0 == first sheet
346                        flow.importer_datamatrix_start = params.datamatrix_start.toInteger() - 1 // 0 == first row
347                        flow.importer_headerrow = params.headerrow.toInteger()
348            flow.importer_entityclass = entityClass.getClass()
349
350                        // Get the header from the Excel file using the arguments given in the first step of the wizard
351                        flow.importer_header = ImporterService.getHeader(session.importer_workbook,
352                                flow.importer_sheetindex,
353                                flow.importer_headerrow,
354                                flow.importer_datamatrix_start,
355                                entityClass)
356
357                        session.importer_datamatrix = ImporterService.getDatamatrix(
358                                session.importer_workbook, flow.importer_header,
359                                flow.importer_sheetindex,
360                                flow.importer_datamatrix_start,
361                                5)
362
363                        flow.importer_templates = Template.get(flow.importer_template_id)
364                        flow.importer_allfieldtypes = "true"
365
366                        return true
367                }
368
369        log.error ".importer wizard not all fields are filled in"
370        this.appendErrorMap(['error': "Not all fields are filled in, please fill in or select all fields"], flash.wizardErrors)
371        return false
372        }
373
374    /**
375     * Save the properties as an import mapping.
376     *
377     * @param Map LocalAttributeMap (the flow scope)
378         * @param Map GrailsParameterMap (the flow parameters = form data)
379         * @returns boolean true if correctly validated, otherwise false
380         */
381    boolean propertiesSaveImportMappingPage(flow, flash, params) {
382        flash.wizardErrors = [:]
383        def isPreferredIdentifier = false
384
385                // Find actual Template object from the chosen template name
386                def template = Template.get(flow.importer_template_id)
387       
388        // Create new ImportMapping instance and persist it
389        def im = new ImportMapping(name:params.mappingname, entity: flow.importer_entityclass, template:template).save()
390       
391
392                params.columnproperty.index.each { columnindex, property ->
393                        // Create an actual class instance of the selected entity with the selected template
394                        // This should be inside the closure because in some cases in the advanced importer, the fields can have different target entities                     
395                        def entityClass = GdtService.getInstanceByEntityName(flow.importer_header[columnindex.toInteger()].entity.getName())
396            def entityObj = entityClass.newInstance(template:template)
397
398            def dontimport = (property == "dontimport") ? true : false
399
400            // Loop through all fields and find the preferred identifier
401            entityObj.giveFields().each {               
402                                isPreferredIdentifier = (it.preferredIdentifier && (it.name == property)) ? true : false
403                        }
404
405            // Create new MappingColumn instance
406            /*def mc = new MappingColumn (name: flow.importer_header[columnindex.toInteger()].name,
407                                        property:property,
408                                        index:columnindex,
409                                        entity:entityClass,
410                                        templatefieldtype:entityObj.giveFieldType(property),
411                                        dontimport: dontimport,
412                                        identifier:isPreferredIdentifier)
413
414            println "storing mapping: " + mc.validate()
415            im.addToMappingColumns(mc)*/
416                }
417    }
418
419        /**
420         * Handle the property mapping page.
421         *
422         * @param Map LocalAttributeMap (the flow scope)
423         * @param Map GrailsParameterMap (the flow parameters = form data)
424         * @returns boolean true if correctly validated, otherwise false
425         */
426        boolean propertiesPage(flow, flash, params) {
427        flash.wizardErrors = [:]
428       
429                // Find actual Template object from the chosen template name
430                def template = Template.get(flow.importer_template_id)
431
432                params.columnproperty.index.each { columnindex, property ->
433                        // Create an actual class instance of the selected entity with the selected template
434                        // This should be inside the closure because in some cases in the advanced importer, the fields can have different target entities
435                        def entityClass = Class.forName(flow.importer_header[columnindex.toInteger()].entity.getName(), true, this.getClass().getClassLoader())
436                        def entityObj = entityClass.newInstance(template: template)
437
438                        // Store the selected property for this column into the column map for the ImporterService
439                        flow.importer_header[columnindex.toInteger()].property = property
440
441                        // Look up the template field type of the target TemplateField and store it also in the map
442                        flow.importer_header[columnindex.toInteger()].templatefieldtype = entityObj.giveFieldType(property)
443
444                        // Is a "Don't import" property assigned to the column?
445                        flow.importer_header[columnindex.toInteger()].dontimport = (property == "dontimport") ? true : false
446
447                        //if it's an identifier set the mapping column true or false
448                        entityObj.giveFields().each {
449                                (it.preferredIdentifier && (it.name == property)) ? flow.importer_header[columnindex.toInteger()].identifier = true : false
450                        }
451                }
452
453                // Import the workbook and store the table with entity records and store the failed cells
454                def (table, failedcells) = ImporterService.importData(flow.importer_template_id,
455                        session.importer_workbook,
456                        flow.importer_sheetindex,
457                        flow.importer_datamatrix_start,
458                        flow.importer_header)
459
460                flow.importer_importeddata = table
461
462        // loop through all entities to validate them and add them to wizardErrors flash when invalid
463        table.each { record ->
464            record.each { entity ->
465                if (!entity.validate()) {
466                                this.appendErrors(entity, flash.wizardErrors, 'entity_' + entity.getIdentifier() + '_')
467                }
468            }
469        }
470
471        flow.importer_failedcells = failedcells
472
473                return true
474        }
475
476        /**
477         * Handle the mapping page.
478         *
479         * @param Map LocalAttributeMap (the flow scope)
480         * @param Map GrailsParameterMap (the flow parameters = form data)
481         * @returns boolean true if correctly validated, otherwise false
482         */
483        boolean mappingsPage(flow, flash, params) {
484                flash.wizardErrors = [:]
485                flow.importer_invalidentities = 0
486
487                flow.importer_importeddata.each { table ->
488                        table.each { entity ->
489                                def invalidfields = 0
490
491                                // Set the fields for this entity by retrieving values from the params
492                                entity.giveFields().each { field ->
493                                        // field of type ontology and value "#invalidterm"?
494                                        if (field.type == org.dbnp.gdt.TemplateFieldType.ONTOLOGYTERM &&
495                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] == "#invalidterm"
496                                        ) {
497                                                invalidfields++
498                                        } else
499                                        if (field.type == org.dbnp.gdt.TemplateFieldType.ONTOLOGYTERM &&
500                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] != "#invalidterm") {
501                                                if (entity) removeFailedCell(flow.importer_failedcells, entity, field)                       
502                                                entity.setFieldValue(field.toString(), params["entity_" + entity.getIdentifier() + "_" + field.escapedName()])
503                                        }
504                                        else
505
506                    if (field.type == org.dbnp.gdt.TemplateFieldType.STRINGLIST &&
507                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] != "#invalidterm") {
508                                                if (entity) removeFailedCell(flow.importer_failedcells, entity, field)                       
509                                                entity.setFieldValue(field.toString(), params["entity_" + entity.getIdentifier() + "_" + field.escapedName()])
510                    } else
511                    if (field.type == org.dbnp.gdt.TemplateFieldType.STRINGLIST &&
512                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] == "#invalidterm"
513                                        ) {
514                                                invalidfields++
515                                        } else
516
517                                                entity.setFieldValue(field.toString(), params["entity_" + entity.getIdentifier() + "_" + field.escapedName()])
518                                }
519
520                                // Determine entity class and add a parent (defined as Study in first step of wizard)
521                                switch (entity.getClass()) {
522                                        case [Subject, Sample, Event]: entity.parent = flow.importer_study
523                                }
524
525                                // Try to validate the entity now all fields have been set
526                                if (!entity.validate() || invalidfields) {
527                                        flow.importer_invalidentities++
528
529                                        // add errors to map
530                                        this.appendErrors(entity, flash.wizardErrors, "entity_" + entity.getIdentifier() + "_")
531
532                                        entity.errors.getAllErrors().each() {
533                                                log.error ".import wizard imported validation error:" + it
534                                        }
535                                } else {
536                                        //removeFailedCell(flow.importer_failedcells, entity)
537                                } // end else if
538
539                        } // end of record
540                } // end of table
541
542                return (flow.importer_invalidentities == 0) ? true : false
543        } // end of method
544
545        /**
546         * @param failedcell failed ontology cells
547         * @param entity entity to remove from the failedcells list
548         */
549        def removeFailedCell(failedcells, entity, field) {
550                // Valid entity, remove it from failedcells
551        def entityidfield = "entity_" + entity.getIdentifier() + "_" + field.name.toLowerCase()
552
553                failedcells.each { record ->
554                        def tempimportcells = []
555
556
557                        record.importcells.each { cell ->               
558                                // remove the cell from the failed cells session
559                                if (cell.entityidentifier != entityidfield) {
560                                        //record.removeFromImportcells(cell)
561                                        tempimportcells.add(cell)
562                                }
563                        }
564
565                        record.importcells = tempimportcells
566                        // } // end of importcells
567                } // end of failedcells
568        }
569
570        /**
571         * Handle the imported entities page.
572         *
573         * @param Map LocalAttributeMap (the flow scope)
574         * @param Map GrailsParameterMap (the flow parameters = form data)
575         * @returns boolean true if correctly validated, otherwise false
576         */
577        boolean importedPage(flow, params) {
578                return true
579        }
580
581        boolean saveEntities(flow, params) {
582                //def (validatedSuccesfully, updatedEntities, failedToPersist) =
583                //try {
584                ImporterService.saveDatamatrix(flow.importer_study, flow.importer_importeddata, authenticationService, log)
585
586                //}
587                //catch (Exception e) {
588//                log.error ".import wizard saveEntities error\n" + e.dump()
589//            }
590
591                //flow.importer_validatedsuccesfully = validatedSuccesfully
592                //flow.importer_failedtopersist = failedToPersist
593                //flow.imported_updatedentities = updatedEntities
594                //flow.importer_totalrows = flow.importer_importeddata.size
595                //flow.importer_referer = ""
596
597                return true
598        }
599
600        /**
601         * append errors of a particular object to a map
602         * @param object
603         * @param map linkedHashMap
604         * @void
605         */
606        def appendErrors(object, map) {
607                this.appendErrorMap(getHumanReadableErrors(object), map)
608        }
609
610        def appendErrors(object, map, prepend) {
611                this.appendErrorMap(getHumanReadableErrors(object), map, prepend)
612        }
613
614        /**
615         * append errors of one map to another map
616         * @param map linkedHashMap
617         * @param map linkedHashMap
618         * @void
619         */
620        def appendErrorMap(map, mapToExtend) {
621                map.each() {key, value ->
622                        mapToExtend[key] = ['key': key, 'value': value, 'dynamic': false]
623                }
624        }
625
626        def appendErrorMap(map, mapToExtend, prepend) {
627                map.each() {key, value ->
628                        mapToExtend[prepend + key] = ['key': key, 'value': value, 'dynamic': true]
629                }
630        }
631
632        /**
633         * transform domain class validation errors into a human readable
634         * linked hash map
635         * @param object validated domain class
636         * @return object  linkedHashMap
637         */
638        def getHumanReadableErrors(object) {
639                def errors = [:]
640                object.errors.getAllErrors().each() { error ->
641                        // error.codes.each() { code -> println code }           
642
643                        // generally speaking g.message(...) should work,
644                        // however it fails in some steps of the wizard
645                        // (add event, add assay, etc) so g is not always
646                        // availably. Using our own instance of the
647                        // validationTagLib instead so it is always
648                        // available to us
649                        errors[error.getArguments()[0]] = validationTagLib.message(error: error)
650                }
651
652                return errors
653        }
654}
Note: See TracBrowser for help on using the repository browser.