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

Last change on this file since 1538 was 1538, checked in by t.w.abma@…, 10 years ago
  • fixed small "existing*" bug/feature with an extra check, so after a refresh the uploaded filename stays correct
  • Property svn:keywords set to Rev Author Date
File size: 23.4 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: 1538 $
17 * $Author: t.w.abma@umcutrecht.nl $
18 * $Date: 2011-02-17 16:28:06 +0000 (do, 17 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
115                if (params.entity) {
116                    flash.importer_datatemplates = Template.findAllByEntity(GdtService.getInstanceByEntity(params.entity.decodeURL()))
117                }
118               
119                flash.importer_params = params               
120
121                // If the file already exists an "existing*" string is added, but we don't
122                // want that after a refresh of the first step in the import wizard, so remove
123                // that string
124                flash.importer_params.importfile = params.importfile.replace('existing*', '')
125
126                                success()
127                        }.to "pageOne"
128
129                        on("next") {
130                                flash.wizardErrors = [:]
131                flash.importer_params = params
132
133                if (params.entity) {
134                    flash.importer_datatemplates = Template.findAllByEntity(GdtService.getInstanceByEntity(params.entity.decodeURL()))
135                }
136
137                                // Study selected?
138                                flow.importer_study = (params.study) ? Study.get(params.study.id.toInteger()) : null
139
140                                // Trying to import data into an existing study?
141                                if (flow.importer_study)
142                                        if (flow.importer_study.canWrite(authenticationService.getLoggedInUser())) 
143                                                fileImportPage(flow, flash, params) ? success() : error()                                       
144                                        else {
145                                                log.error ".importer wizard wrong permissions"
146                                                this.appendErrorMap(['error': "You don't have the right permissions"], flash.wizardErrors)
147                                                error()
148                                        }
149                                else {
150                                        fileImportPage(flow, flash, params) ? success() : error()
151                                }
152
153                                // put your bussiness logic (if applicable) in here
154                        }.to "pageTwo"
155                }
156
157                // Property to column assignment page
158                pageTwo {
159                        render(view: "_page_two")
160                        onRender {
161                                log.info ".import wizard properties page"
162               
163                def template = Template.get(flow.importer_template_id)
164
165                                // Grom a development message
166                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial: pages/_page_two.gsp".grom()
167
168                flow.importer_importmappings = ImportMapping.findAllByTemplate(template)
169
170                                flow.page = 2
171                                success()
172                        }
173            on("refresh") {
174                def template = Template.get(flow.importer_template_id)
175                flow.importer_importmappings = ImportMapping.findAllByTemplate(template)
176
177                // a name was given to the current property mapping, try to store it
178                if (params.mappingname) {
179                    flash.importer_columnproperty = params.columnproperty
180                    propertiesSaveImportMappingPage(flow, flash, params)                   
181                } else // trying to load an existing import mapping
182                if (params.importmapping_id) {
183                    propertiesLoadImportMappingPage(flow, flash, params)
184                }
185               
186                if (params.fuzzymatching == "true")
187                    flow.importer_fuzzymatching="true" else
188                    flow.importer_fuzzymatching="false"
189
190                success()
191                        }.to "pageTwo"
192
193                        on("next") {
194                                if (propertiesPage(flow, flash, params)) {
195                                        success()
196                                } else {
197                                        log.error ".import wizard, properties are set wrong"
198                                        error()
199                                }
200                        }.to "pageThree"
201                        on("previous").to "pageOne"
202                }
203
204                // Mapping page
205                pageThree {
206                        render(view: "_page_three")
207                        onRender {
208                                log.info ".import wizard mapping page"
209                                // Grom a development message
210                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_page_three.gsp".grom()
211
212                                flow.page = 3
213                                success()
214                        }
215                        on("refresh") {
216                                success()
217                        }.to "pageThree"
218                        on("next") {
219                                if (mappingsPage(flow, flash, params)) {
220                                        flow.page = 4
221                                        success()
222                                } else {
223                                        log.error ".import wizard mapping error, could not validate all entities"
224                                        error()
225                                }
226                        }.to "save"
227                        on("previous").to "pageTwo"
228                }
229
230                // Imported data overview page
231                pageFour {
232                        render(view: "_page_four")
233                        onRender {
234                                // Grom a development message
235                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_page_four.gsp".grom()
236
237                                flow.page = 4
238                                success()
239                        }
240                        on("next") {
241                                if (importedPage(flow, params)) {
242                                        flow.page = 4
243                                        success()
244                                } else {
245                                        log.error ".import wizard imported error, something went wrong showing the imported entities"
246                                        error()
247                                }
248                        }.to "save"
249                        on("previous").to "pageThree"
250                }
251
252                // Save the imported data
253                save {
254                        action {
255                                // here you can validate and save the
256                                // instances you have created in the
257                                // ajax flow.
258                                // Grom a development message
259                                if (pluginManager.getGrailsPlugin('grom')) ".persisting instances to the database...".grom()
260
261                                if (saveEntities(flow, params)) {
262                                        //if (ImporterService.saveDatamatrix(flow.importer_study, flow.importer_importeddata)) {
263                                        //log.error ".import wizard succesfully persisted all entities"
264                                        //def session = sessionFactory.getCurrentSession()
265                                        //session.clear()
266                                        success()
267                                } else {
268                                        log.error ".import wizard, could not save entities:\n" + e.dump()
269                                        flow.page = 4
270                                        error()
271                                }
272                        }
273                        on("error").to "error"
274                        on(Exception).to "error"
275                        on("success").to "finalPage"
276                }
277
278                // render errors
279                error {
280                        render(view: "_error")
281                        onRender {
282                                // Grom a development message
283                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_error.gsp".grom()
284
285                                // set page to 4 so that the navigation
286                                // works (it is disabled on the final page)
287                                flow.page = 4
288                        }
289                        on("next").to "save"
290                        on("previous").to "pageFour"
291                }
292
293                // last wizard page
294                finalPage {
295                        render(view: "_final_page")
296                        onRender {
297                                // Grom a development message
298                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_final_page.gsp".grom()
299                                success()
300                        }
301                        onEnd {
302                                // clean flow scope
303                                flow.clear()
304                        }
305                }
306        }
307
308    def propertiesManager = {
309        render (view:"common/_propertiesManager")
310    }
311
312        /**
313         * Return templates which belong to a certain entity type
314         *
315         * @param entity entity name string (Sample, Subject, Study et cetera)
316         * @return JSON object containing the found templates
317         */
318        def ajaxGetTemplatesByEntity = {
319                // fetch all templates for a specific entity
320                def templates = Template.findAllByEntity(GdtService.getInstanceByEntity(params.entity.decodeURL()))
321
322
323                // render as JSON
324                render templates as JSON
325        }
326
327        /**
328         * Handle the file import page.
329         *
330         * @param Map LocalAttributeMap (the flow scope)
331         * @param Map GrailsParameterMap (the flow parameters = form data)
332         * @returns boolean true if correctly validated, otherwise false
333         */
334        boolean fileImportPage(flow, flash, params) {
335                def importedfile = fileService.get(params['importfile'])
336                //fileService.delete(YourFile)
337
338        if (importedfile.exists()) {
339            try {
340                session.importer_workbook = ImporterService.getWorkbook(new FileInputStream(importedfile))
341            } catch (Exception e) {
342                log.error ".importer wizard could not load file: " + e
343                this.appendErrorMap(['error': "Wrong file (format), the importer requires an Excel file as input"], flash.wizardErrors)
344                return false
345            }
346        }
347
348                if (params.entity && params.template_id) {           
349                       
350            try {
351                session.importer_workbook = ImporterService.getWorkbook(new FileInputStream(importedfile))
352            } catch (Exception e) {
353                log.error ".importer wizard could not load file: " + e
354                this.appendErrorMap(['error': "Excel file required as input"], flash.wizardErrors)
355                return false
356            }
357
358                        def selectedentities = []
359
360                        def entityName = GdtService.decryptEntity(params.entity.decodeURL())
361                        def entityClass = GdtService.getInstanceByEntityName(entityName)
362
363                        // Initialize some session variables
364                        //flow.importer_workbook = wb // workbook object must be serialized for this to work
365
366                        flow.importer_template_id = params.template_id
367                        flow.importer_sheetindex = params.sheetindex.toInteger() - 1 // 0 == first sheet
368                        flow.importer_datamatrix_start = params.datamatrix_start.toInteger() - 1 // 0 == first row
369                        flow.importer_headerrow = params.headerrow.toInteger()
370            flow.importer_entityclass = entityClass
371
372                        // Get the header from the Excel file using the arguments given in the first step of the wizard
373                        flow.importer_header = ImporterService.getHeader(session.importer_workbook,
374                                flow.importer_sheetindex,
375                                flow.importer_headerrow,
376                                flow.importer_datamatrix_start,
377                                entityClass)
378
379                        session.importer_datamatrix = ImporterService.getDatamatrix(
380                                session.importer_workbook, flow.importer_header,
381                                flow.importer_sheetindex,
382                                flow.importer_datamatrix_start,
383                                5)
384
385                        flow.importer_templates = Template.get(flow.importer_template_id)
386                        flow.importer_allfieldtypes = "true"
387
388                        return true
389                }
390
391
392        log.error ".importer wizard not all fields are filled in"
393        this.appendErrorMap(['error': "Not all fields are filled in, please fill in or select all fields"], flash.wizardErrors)
394        return false
395        }
396
397    /**
398     * Load an existing import mapping
399     *
400     * @param Map LocalAttributeMap (the flow scope)
401         * @param Map GrailsParameterMap (the flow parameters = form data)
402         * @returns boolean true if correctly validated, otherwise false
403         */
404    boolean propertiesLoadImportMappingPage(flow, flash, params) {
405        def im = ImportMapping.get(params.importmapping_id.toInteger())
406        im.refresh()
407
408        im.mappingcolumns.each { mappingcolumn ->
409            //def mc = new MappingColumn()
410            //mc.properties = mappingcolumn.properties
411
412            flow.importer_header[mappingcolumn.index.toInteger()] = mappingcolumn           
413        }
414    }
415
416    /**
417     * Save the properties as an import mapping.
418     *
419     * @param Map LocalAttributeMap (the flow scope)
420         * @param Map GrailsParameterMap (the flow parameters = form data)
421         * @returns boolean true if correctly validated, otherwise false
422         */
423    boolean propertiesSaveImportMappingPage(flow, flash, params) {
424        flash.wizardErrors = [:]
425        def isPreferredIdentifier = false
426
427                // Find actual Template object from the chosen template name
428                def template = Template.get(flow.importer_template_id)
429       
430        // Create new ImportMapping instance and persist it
431        def im = new ImportMapping(name:params.mappingname, entity: flow.importer_entityclass, template:template).save()       
432
433                params.columnproperty.index.each { columnindex, property ->
434                        // Create an actual class instance of the selected entity with the selected template
435                        // This should be inside the closure because in some cases in the advanced importer, the fields can have different target entities                     
436                        //def entityClass = GdtService.getInstanceByEntityName(flow.importer_header[columnindex.toInteger()].entity.getName())           
437            def entityObj = flow.importer_entityclass.newInstance(template:template)
438
439            def dontimport = (property == "dontimport") ? true : false
440
441            // Loop through all fields and find the preferred identifier
442            entityObj.giveFields().each {               
443                                isPreferredIdentifier = (it.preferredIdentifier && (it.name == property)) ? true : false
444                        }
445
446            // Create new MappingColumn instance
447            def mc = new MappingColumn (importmapping:im,
448                                        name: flow.importer_header[columnindex.toInteger()].name,
449                                        property:property,
450                                        index:columnindex,
451                                        entityclass:flow.importer_entityclass,
452                                        templatefieldtype:entityObj.giveFieldType(property),
453                                        dontimport: dontimport,
454                                        identifier:isPreferredIdentifier)
455
456            // Save mappingcolumn
457            if (mc.validate()) {
458                im.addToMappingcolumns(mc)               
459            }
460            else {
461                mc.errors.allErrors.each {
462                    println it
463                }
464            }
465
466            // Save importmapping
467            if (im.validate()) {
468                try {
469                    im.save(flush:true)
470                } catch (Exception e) {
471                    //getNextException
472                    log.error "importer wizard save importmapping error: " + e
473                }
474            }
475            else {
476                im.errors.allErrors.each {
477                    println it
478                }
479            }
480           
481                }
482    }
483
484        /**
485         * Handle the property mapping page.
486         *
487         * @param Map LocalAttributeMap (the flow scope)
488         * @param Map GrailsParameterMap (the flow parameters = form data)
489         * @returns boolean true if correctly validated, otherwise false
490         */
491        boolean propertiesPage(flow, flash, params) {
492        flash.wizardErrors = [:]
493       
494                // Find actual Template object from the chosen template name
495                def template = Template.get(flow.importer_template_id)
496
497                params.columnproperty.index.each { columnindex, property ->
498                        // Create an actual class instance of the selected entity with the selected template
499                        // This should be inside the closure because in some cases in the advanced importer, the fields can have different target entities
500                        def entityClass = Class.forName(flow.importer_header[columnindex.toInteger()].entityclass.getName(), true, this.getClass().getClassLoader())
501                        def entityObj = entityClass.newInstance(template: template)
502
503                        // Store the selected property for this column into the column map for the ImporterService
504                        flow.importer_header[columnindex.toInteger()].property = property
505
506                        // Look up the template field type of the target TemplateField and store it also in the map
507                        flow.importer_header[columnindex.toInteger()].templatefieldtype = entityObj.giveFieldType(property)
508
509                        // Is a "Don't import" property assigned to the column?
510                        flow.importer_header[columnindex.toInteger()].dontimport = (property == "dontimport") ? true : false
511
512                        //if it's an identifier set the mapping column true or false
513                        entityObj.giveFields().each {
514                                (it.preferredIdentifier && (it.name == property)) ? flow.importer_header[columnindex.toInteger()].identifier = true : false
515                        }
516                }
517
518                // Import the workbook and store the table with entity records and store the failed cells
519                def (table, failedcells) = ImporterService.importData(flow.importer_template_id,
520                        session.importer_workbook,
521                        flow.importer_sheetindex,
522                        flow.importer_datamatrix_start,
523                        flow.importer_header)
524
525                flow.importer_importeddata = table
526
527        // loop through all entities to validate them and add them to wizardErrors flash when invalid
528        table.each { record ->
529            record.each { entity ->
530                if (!entity.validate()) {
531                                this.appendErrors(entity, flash.wizardErrors, 'entity_' + entity.getIdentifier() + '_')
532                }
533            }
534        }
535
536        flow.importer_failedcells = failedcells
537
538                return true
539        }
540
541        /**
542         * Handle the mapping page.
543         *
544         * @param Map LocalAttributeMap (the flow scope)
545         * @param Map GrailsParameterMap (the flow parameters = form data)
546         * @returns boolean true if correctly validated, otherwise false
547         */
548        boolean mappingsPage(flow, flash, params) {
549                flash.wizardErrors = [:]
550                flow.importer_invalidentities = 0
551
552                flow.importer_importeddata.each { table ->
553                        table.each { entity ->
554                                def invalidfields = 0
555
556                                // Set the fields for this entity by retrieving values from the params
557                                entity.giveFields().each { field ->
558                                        // field of type ontology and value "#invalidterm"?
559                                        if (field.type == org.dbnp.gdt.TemplateFieldType.ONTOLOGYTERM &&
560                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] == "#invalidterm"
561                                        ) {
562                                                invalidfields++
563                                        } else
564                                        if (field.type == org.dbnp.gdt.TemplateFieldType.ONTOLOGYTERM &&
565                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] != "#invalidterm") {
566                                                if (entity) removeFailedCell(flow.importer_failedcells, entity, field)                       
567                                                entity.setFieldValue(field.toString(), params["entity_" + entity.getIdentifier() + "_" + field.escapedName()])
568                                        }
569                                        else
570
571                    if (field.type == org.dbnp.gdt.TemplateFieldType.STRINGLIST &&
572                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] != "#invalidterm") {
573                                                if (entity) removeFailedCell(flow.importer_failedcells, entity, field)                       
574                                                entity.setFieldValue(field.toString(), params["entity_" + entity.getIdentifier() + "_" + field.escapedName()])
575                    } else
576                    if (field.type == org.dbnp.gdt.TemplateFieldType.STRINGLIST &&
577                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] == "#invalidterm"
578                                        ) {
579                                                invalidfields++
580                                        } else
581
582                                                entity.setFieldValue(field.toString(), params["entity_" + entity.getIdentifier() + "_" + field.escapedName()])
583                                }
584
585                                // Determine entity class and add a parent (defined as Study in first step of wizard)
586                                switch (entity.getClass()) {
587                                        case [Subject, Sample, Event]: entity.parent = flow.importer_study
588                                }
589
590                                // Try to validate the entity now all fields have been set
591                                if (!entity.validate() || invalidfields) {
592                                        flow.importer_invalidentities++
593
594                                        // add errors to map
595                                        this.appendErrors(entity, flash.wizardErrors, "entity_" + entity.getIdentifier() + "_")
596
597                                        entity.errors.getAllErrors().each() {
598                                                log.error ".import wizard imported validation error:" + it
599                                        }
600                                } else {
601                                        //removeFailedCell(flow.importer_failedcells, entity)
602                                } // end else if
603
604                        } // end of record
605                } // end of table
606
607                return (flow.importer_invalidentities == 0) ? true : false
608        } // end of method
609
610        /**
611         * @param failedcell failed ontology cells
612         * @param entity entity to remove from the failedcells list
613         */
614        def removeFailedCell(failedcells, entity, field) {
615                // Valid entity, remove it from failedcells
616        def entityidfield = "entity_" + entity.getIdentifier() + "_" + field.name.toLowerCase()
617
618                failedcells.each { record ->
619                        def tempimportcells = []
620
621
622                        record.importcells.each { cell ->               
623                                // remove the cell from the failed cells session
624                                if (cell.entityidentifier != entityidfield) {
625                                        //record.removeFromImportcells(cell)
626                                        tempimportcells.add(cell)
627                                }
628                        }
629
630                        record.importcells = tempimportcells
631                        // } // end of importcells
632                } // end of failedcells
633        }
634
635        /**
636         * Handle the imported entities page.
637         *
638         * @param Map LocalAttributeMap (the flow scope)
639         * @param Map GrailsParameterMap (the flow parameters = form data)
640         * @returns boolean true if correctly validated, otherwise false
641         */
642        boolean importedPage(flow, params) {
643                return true
644        }
645
646        boolean saveEntities(flow, params) {
647                //def (validatedSuccesfully, updatedEntities, failedToPersist) =
648                //try {
649                ImporterService.saveDatamatrix(flow.importer_study, flow.importer_importeddata, authenticationService, log)
650
651                //}
652                //catch (Exception e) {
653//                log.error ".import wizard saveEntities error\n" + e.dump()
654//            }
655
656                //flow.importer_validatedsuccesfully = validatedSuccesfully
657                //flow.importer_failedtopersist = failedToPersist
658                //flow.imported_updatedentities = updatedEntities
659                //flow.importer_totalrows = flow.importer_importeddata.size
660                //flow.importer_referer = ""
661
662                return true
663        }
664
665        /**
666         * append errors of a particular object to a map
667         * @param object
668         * @param map linkedHashMap
669         * @void
670         */
671        def appendErrors(object, map) {
672                this.appendErrorMap(getHumanReadableErrors(object), map)
673        }
674
675        def appendErrors(object, map, prepend) {
676                this.appendErrorMap(getHumanReadableErrors(object), map, prepend)
677        }
678
679        /**
680         * append errors of one map to another map
681         * @param map linkedHashMap
682         * @param map linkedHashMap
683         * @void
684         */
685        def appendErrorMap(map, mapToExtend) {
686                map.each() {key, value ->
687                        mapToExtend[key] = ['key': key, 'value': value, 'dynamic': false]
688                }
689        }
690
691        def appendErrorMap(map, mapToExtend, prepend) {
692                map.each() {key, value ->
693                        mapToExtend[prepend + key] = ['key': key, 'value': value, 'dynamic': true]
694                }
695        }
696
697        /**
698         * transform domain class validation errors into a human readable
699         * linked hash map
700         * @param object validated domain class
701         * @return object  linkedHashMap
702         */
703        def getHumanReadableErrors(object) {
704                def errors = [:]
705                object.errors.getAllErrors().each() { error ->
706                        // error.codes.each() { code -> println code }           
707
708                        // generally speaking g.message(...) should work,
709                        // however it fails in some steps of the wizard
710                        // (add event, add assay, etc) so g is not always
711                        // availably. Using our own instance of the
712                        // validationTagLib instead so it is always
713                        // available to us
714                        errors[error.getArguments()[0]] = validationTagLib.message(error: error)
715                }
716
717                return errors
718        }
719
720}
Note: See TracBrowser for help on using the repository browser.