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

Last change on this file since 1505 was 1505, checked in by t.w.abma@…, 10 years ago
  • fuzzy matching and don't import improved in the properties step of the importer wizard, by default properties are not matched against the Excel fields, but one can now manually apply fuzzy matching
  • added initial fields to ImportMapping? domain
  • Property svn:keywords set to Rev Author Date
File size: 18.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: 1505 $
17 * $Author: t.w.abma@umcutrecht.nl $
18 * $Date: 2011-02-08 14:53:49 +0000 (di, 08 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.page = 2
151                                success()
152                        }
153            on("refresh") {
154                flow.importer_fuzzymatching="true"
155                                success()
156                        }.to "pageTwo"
157
158                        on("next") {
159                                if (propertiesPage(flow, flash, params)) {
160                                        success()
161                                } else {
162                                        log.error ".import wizard, properties are set wrong"
163                                        error()
164                                }
165                        }.to "pageThree"
166                        on("previous").to "pageOne"
167                }
168
169                // Mapping page
170                pageThree {
171                        render(view: "_page_three")
172                        onRender {
173                                log.info ".import wizard mapping page"
174                                // Grom a development message
175                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_page_three.gsp".grom()
176
177                                flow.page = 3
178                                success()
179                        }
180                        on("refresh") {
181                                success()
182                        }.to "pageThree"
183                        on("next") {
184                                if (mappingsPage(flow, flash, params)) {
185                                        flow.page = 4
186                                        success()
187                                } else {
188                                        log.error ".import wizard mapping error, could not validate all entities"
189                                        error()
190                                }
191                        }.to "save"
192                        on("previous").to "pageTwo"
193                }
194
195                // Imported data overview page
196                pageFour {
197                        render(view: "_page_four")
198                        onRender {
199                                // Grom a development message
200                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_page_four.gsp".grom()
201
202                                flow.page = 4
203                                success()
204                        }
205                        on("next") {
206                                if (importedPage(flow, params)) {
207                                        flow.page = 4
208                                        success()
209                                } else {
210                                        log.error ".import wizard imported error, something went wrong showing the imported entities"
211                                        error()
212                                }
213                        }.to "save"
214                        on("previous").to "pageThree"
215                }
216
217                // Save the imported data
218                save {
219                        action {
220                                // here you can validate and save the
221                                // instances you have created in the
222                                // ajax flow.
223                                // Grom a development message
224                                if (pluginManager.getGrailsPlugin('grom')) ".persisting instances to the database...".grom()
225
226                                if (saveEntities(flow, params)) {
227                                        //if (ImporterService.saveDatamatrix(flow.importer_study, flow.importer_importeddata)) {
228                                        //log.error ".import wizard succesfully persisted all entities"
229                                        //def session = sessionFactory.getCurrentSession()
230                                        //session.clear()
231                                        success()
232                                } else {
233                                        log.error ".import wizard, could not save entities:\n" + e.dump()
234                                        flow.page = 4
235                                        error()
236                                }
237                        }
238                        on("error").to "error"
239                        on(Exception).to "error"
240                        on("success").to "finalPage"
241                }
242
243                // render errors
244                error {
245                        render(view: "_error")
246                        onRender {
247                                // Grom a development message
248                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_error.gsp".grom()
249
250                                // set page to 4 so that the navigation
251                                // works (it is disabled on the final page)
252                                flow.page = 4
253                        }
254                        on("next").to "save"
255                        on("previous").to "pageFour"
256                }
257
258                // last wizard page
259                finalPage {
260                        render(view: "_final_page")
261                        onRender {
262                                // Grom a development message
263                                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_final_page.gsp".grom()
264                                success()
265                        }
266                        onEnd {
267                                // clean flow scope
268                                flow.clear()
269                        }
270                }
271        }
272
273        /**
274         * Return templates which belong to a certain entity type
275         *
276         * @param entity entity name string (Sample, Subject, Study et cetera)
277         * @return JSON object containing the found templates
278         */
279        def ajaxGetTemplatesByEntity = {
280                // fetch all templates for a specific entity
281                def templates = Template.findAllByEntity(GdtService.getInstanceByEntity(params.entity.decodeURL()))
282
283
284                // render as JSON
285                render templates as JSON
286        }
287
288        /**
289         * Handle the file import page.
290         *
291         * @param Map LocalAttributeMap (the flow scope)
292         * @param Map GrailsParameterMap (the flow parameters = form data)
293         * @returns boolean true if correctly validated, otherwise false
294         */
295        boolean fileImportPage(flow, flash, params) {
296                def importedfile = fileService.get(params['importfile'])
297                //fileService.delete(YourFile)
298
299        if (importedfile.exists()) {
300            try {
301                session.importer_workbook = ImporterService.getWorkbook(new FileInputStream(importedfile))
302            } catch (Exception e) {
303                log.error ".importer wizard could not load file: " + e
304                this.appendErrorMap(['error': "Wrong file (format), the importer requires an Excel file as input"], flash.wizardErrors)
305                return false
306            }
307        }
308
309                if (params.entity && params.template_id) {           
310                       
311            try {
312                session.importer_workbook = ImporterService.getWorkbook(new FileInputStream(importedfile))
313            } catch (Exception e) {
314                log.error ".importer wizard could not load file: " + e
315                this.appendErrorMap(['error': "Excel file required as input"], flash.wizardErrors)
316                return false
317            }
318
319                        def selectedentities = []
320
321                        def entityName = GdtService.decryptEntity(params.entity.decodeURL())
322                        def entityClass = GdtService.getInstanceByEntityName(entityName)
323
324                        // Initialize some session variables
325                        //flow.importer_workbook = wb // workbook object must be serialized for this to work
326
327                        flow.importer_template_id = params.template_id
328                        flow.importer_sheetindex = params.sheetindex.toInteger() - 1 // 0 == first sheet
329                        flow.importer_datamatrix_start = params.datamatrix_start.toInteger() - 1 // 0 == first row
330                        flow.importer_headerrow = params.headerrow.toInteger()
331
332                        // Get the header from the Excel file using the arguments given in the first step of the wizard
333                        flow.importer_header = ImporterService.getHeader(session.importer_workbook,
334                                flow.importer_sheetindex,
335                                flow.importer_headerrow,
336                                flow.importer_datamatrix_start,
337                                entityClass)
338
339                        // Initialize 'selected entities', used to show entities above the columns
340                        flow.importer_header.each {
341                                selectedentities.add([name: entityName, columnindex: it.key.toInteger()])
342                        }
343
344                        flow.importer_selectedentities = selectedentities
345
346                        session.importer_datamatrix = ImporterService.getDatamatrix(
347                                session.importer_workbook, flow.importer_header,
348                                flow.importer_sheetindex,
349                                flow.importer_datamatrix_start,
350                                5)
351
352                        flow.importer_templates = Template.get(flow.importer_template_id)
353                        flow.importer_allfieldtypes = "true"
354
355                        return true
356                }
357
358        log.error ".importer wizard not all fields are filled in"
359        this.appendErrorMap(['error': "Not all fields are filled in, please fill in or select all fields"], flash.wizardErrors)
360        return false
361        }
362
363        /**
364         * Handle the property mapping page.
365         *
366         * @param Map LocalAttributeMap (the flow scope)
367         * @param Map GrailsParameterMap (the flow parameters = form data)
368         * @returns boolean true if correctly validated, otherwise false
369         */
370        boolean propertiesPage(flow, flash, params) {
371        flash.wizardErrors = [:]
372       
373                // Find actual Template object from the chosen template name
374                def template = Template.get(flow.importer_template_id)
375
376                params.columnproperty.index.each { columnindex, property ->
377                        // Create an actual class instance of the selected entity with the selected template
378                        // This should be inside the closure because in some cases in the advanced importer, the fields can have different target entities
379                        def entityClass = Class.forName(flow.importer_header[columnindex.toInteger()].entity.getName(), true, this.getClass().getClassLoader())
380                        def entityObj = entityClass.newInstance(template: template)
381
382                        // Store the selected property for this column into the column map for the ImporterService
383                        flow.importer_header[columnindex.toInteger()].property = property
384
385                        // Look up the template field type of the target TemplateField and store it also in the map
386                        flow.importer_header[columnindex.toInteger()].templatefieldtype = entityObj.giveFieldType(property)
387
388                        // Is a "Don't import" property assigned to the column?
389                        flow.importer_header[columnindex.toInteger()].dontimport = (property == "dontimport") ? true : false
390
391                        //if it's an identifier set the mapping column true or false
392                        entityObj.giveFields().each {
393                                (it.preferredIdentifier && (it.name == property)) ? flow.importer_header[columnindex.toInteger()].identifier = true : false
394                        }
395                }
396
397                // Import the workbook and store the table with entity records and store the failed cells
398                def (table, failedcells) = ImporterService.importData(flow.importer_template_id,
399                        session.importer_workbook,
400                        flow.importer_sheetindex,
401                        flow.importer_datamatrix_start,
402                        flow.importer_header)
403
404                flow.importer_importeddata = table
405
406        // loop through all entities to validate them and add them to wizardErrors flash when invalid
407        table.each { record ->
408            record.each { entity ->
409                if (!entity.validate()) {
410                                this.appendErrors(entity, flash.wizardErrors, 'entity_' + entity.getIdentifier() + '_')
411                }
412            }
413        }
414
415        flow.importer_failedcells = failedcells
416
417                return true
418        }
419
420        /**
421         * Handle the mapping page.
422         *
423         * @param Map LocalAttributeMap (the flow scope)
424         * @param Map GrailsParameterMap (the flow parameters = form data)
425         * @returns boolean true if correctly validated, otherwise false
426         */
427        boolean mappingsPage(flow, flash, params) {
428                flash.wizardErrors = [:]
429                flow.importer_invalidentities = 0
430
431                flow.importer_importeddata.each { table ->
432                        table.each { entity ->
433                                def invalidfields = 0
434
435                                // Set the fields for this entity by retrieving values from the params
436                                entity.giveFields().each { field ->
437                                        // field of type ontology and value "#invalidterm"?
438                                        if (field.type == org.dbnp.gdt.TemplateFieldType.ONTOLOGYTERM &&
439                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] == "#invalidterm"
440                                        ) {
441                                                invalidfields++
442                                        } else
443                                        if (field.type == org.dbnp.gdt.TemplateFieldType.ONTOLOGYTERM &&
444                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] != "#invalidterm") {
445                                                if (entity) removeFailedCell(flow.importer_failedcells, entity, field)                       
446                                                entity.setFieldValue(field.toString(), params["entity_" + entity.getIdentifier() + "_" + field.escapedName()])
447                                        }
448                                        else
449
450                    if (field.type == org.dbnp.gdt.TemplateFieldType.STRINGLIST &&
451                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] != "#invalidterm") {
452                                                if (entity) removeFailedCell(flow.importer_failedcells, entity, field)                       
453                                                entity.setFieldValue(field.toString(), params["entity_" + entity.getIdentifier() + "_" + field.escapedName()])
454                    } else
455                    if (field.type == org.dbnp.gdt.TemplateFieldType.STRINGLIST &&
456                                                params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] == "#invalidterm"
457                                        ) {
458                                                invalidfields++
459                                        } else
460
461                                                entity.setFieldValue(field.toString(), params["entity_" + entity.getIdentifier() + "_" + field.escapedName()])
462                                }
463
464                                // Determine entity class and add a parent (defined as Study in first step of wizard)
465                                switch (entity.getClass()) {
466                                        case [Subject, Sample, Event]: entity.parent = flow.importer_study
467                                }
468
469                                // Try to validate the entity now all fields have been set
470                                if (!entity.validate() || invalidfields) {
471                                        flow.importer_invalidentities++
472
473                                        // add errors to map
474                                        this.appendErrors(entity, flash.wizardErrors, "entity_" + entity.getIdentifier() + "_")
475
476                                        entity.errors.getAllErrors().each() {
477                                                log.error ".import wizard imported validation error:" + it
478                                        }
479                                } else {
480                                        //removeFailedCell(flow.importer_failedcells, entity)
481                                } // end else if
482
483                        } // end of record
484                } // end of table
485
486                return (flow.importer_invalidentities == 0) ? true : false
487        } // end of method
488
489        /**
490         * @param failedcell failed ontology cells
491         * @param entity entity to remove from the failedcells list
492         */
493        def removeFailedCell(failedcells, entity, field) {
494                // Valid entity, remove it from failedcells
495        def entityidfield = "entity_" + entity.getIdentifier() + "_" + field.name.toLowerCase()
496
497                failedcells.each { record ->
498                        def tempimportcells = []
499
500
501                        record.importcells.each { cell ->               
502                                // remove the cell from the failed cells session
503                                if (cell.entityidentifier != entityidfield) {
504                                        //record.removeFromImportcells(cell)
505                                        tempimportcells.add(cell)
506                                }
507                        }
508
509                        record.importcells = tempimportcells
510                        // } // end of importcells
511                } // end of failedcells
512        }
513
514        /**
515         * Handle the imported entities page.
516         *
517         * @param Map LocalAttributeMap (the flow scope)
518         * @param Map GrailsParameterMap (the flow parameters = form data)
519         * @returns boolean true if correctly validated, otherwise false
520         */
521        boolean importedPage(flow, params) {
522                return true
523        }
524
525        boolean saveEntities(flow, params) {
526                //def (validatedSuccesfully, updatedEntities, failedToPersist) =
527                //try {
528                ImporterService.saveDatamatrix(flow.importer_study, flow.importer_importeddata, authenticationService, log)
529
530                //}
531                //catch (Exception e) {
532//                log.error ".import wizard saveEntities error\n" + e.dump()
533//            }
534
535                //flow.importer_validatedsuccesfully = validatedSuccesfully
536                //flow.importer_failedtopersist = failedToPersist
537                //flow.imported_updatedentities = updatedEntities
538                //flow.importer_totalrows = flow.importer_importeddata.size
539                //flow.importer_referer = ""
540
541                return true
542        }
543
544        /**
545         * append errors of a particular object to a map
546         * @param object
547         * @param map linkedHashMap
548         * @void
549         */
550        def appendErrors(object, map) {
551                this.appendErrorMap(getHumanReadableErrors(object), map)
552        }
553
554        def appendErrors(object, map, prepend) {
555                this.appendErrorMap(getHumanReadableErrors(object), map, prepend)
556        }
557
558        /**
559         * append errors of one map to another map
560         * @param map linkedHashMap
561         * @param map linkedHashMap
562         * @void
563         */
564        def appendErrorMap(map, mapToExtend) {
565                map.each() {key, value ->
566                        mapToExtend[key] = ['key': key, 'value': value, 'dynamic': false]
567                }
568        }
569
570        def appendErrorMap(map, mapToExtend, prepend) {
571                map.each() {key, value ->
572                        mapToExtend[prepend + key] = ['key': key, 'value': value, 'dynamic': true]
573                }
574        }
575
576        /**
577         * transform domain class validation errors into a human readable
578         * linked hash map
579         * @param object validated domain class
580         * @return object  linkedHashMap
581         */
582        def getHumanReadableErrors(object) {
583                def errors = [:]
584                object.errors.getAllErrors().each() { error ->
585                        // error.codes.each() { code -> println code }           
586
587                        // generally speaking g.message(...) should work,
588                        // however it fails in some steps of the wizard
589                        // (add event, add assay, etc) so g is not always
590                        // availably. Using our own instance of the
591                        // validationTagLib instead so it is always
592                        // available to us
593                        errors[error.getArguments()[0]] = validationTagLib.message(error: error)
594                }
595
596                return errors
597        }
598}
Note: See TracBrowser for help on using the repository browser.