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

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