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

Last change on this file since 1388 was 1388, checked in by t.w.abma@…, 9 years ago
  • added popup dialog with warning for first page of importer when not all fields are filled in or selected
  • Property svn:keywords set to Author Date Rev
File size: 23.3 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: 1388 $
25 * $Author: t.w.abma@umcutrecht.nl $
26 * $Date: 2011-01-13 09:53:50 +0000 (do, 13 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                    println "properties are 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                    flow.page = 4
285                    error()
286                }
287            }
288            on("error").to "error"
289            on(Exception).to "error"
290            on("success").to "finalPage"
291        }
292
293        // render errors
294        error {                       
295            render(view: "_error")
296            onRender {
297               
298                // Grom a development message
299                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_error.gsp".grom()
300
301                // set page to 4 so that the navigation
302                // works (it is disabled on the final page)
303                flow.page = 4
304            }
305            on("next").to "save"
306            on("previous").to "pageFour"
307            on("toPageOne").to "pageOne"
308            on("toPageTwo").to "pageTwo"
309            on("toPageThree").to "pageThree"
310            on("toPageFour").to "pageFour"
311            on("toPageFive").to "save"
312        }
313
314        // last wizard page
315        finalPage {
316            render(view: "_final_page")
317            onRender {
318                // Grom a development message
319                if (pluginManager.getGrailsPlugin('grom')) ".rendering the partial pages/_final_page.gsp".grom()
320                               
321                success()
322            }
323        }
324    }
325
326    /**
327     * Return templates which belong to a certain entity type
328     *
329     * @param entity entity name string (Sample, Subject, Study et cetera)
330     * @return JSON object containing the found templates
331     */
332    def ajaxGetTemplatesByEntity = {       
333        def entityName = Blowfish.decryptBase64(           
334            URLDecoder.decode(params.entity),
335            grailsApplication.config.crypto.shared.secret
336        )
337
338        //def entityClass = grailsApplication.config.gscf.domain.importableEntities.get(params.entity).entity
339        def entityClass = entityName
340
341        // fetch all templates for a specific entity
342        def templates = Template.findAllByEntity(Class.forName(entityClass, true, this.getClass().getClassLoader()))
343
344        // render as JSON
345        render templates as JSON
346    }
347
348    /**
349     * Handle the file import page.
350     *
351     * @param Map LocalAttributeMap (the flow scope)
352     * @param Map GrailsParameterMap (the flow parameters = form data)
353     * @returns boolean true if correctly validated, otherwise false
354     */
355    boolean fileImportPage(flow, params) {
356        def importedfile = fileService.get(params['importfile'])
357        //fileService.delete(YourFile)
358
359        if (params.entity && params.template_id && importedfile.exists()) {
360            // create a workbook instance of the file
361            session.importer_workbook = ImporterService.getWorkbook(new FileInputStream(importedfile))
362           
363            def selectedentities = []
364           
365            def entityName = Blowfish.decryptBase64(
366                URLDecoder.decode(params.entity),
367                grailsApplication.config.crypto.shared.secret
368            )
369           
370            def entityClass = Class.forName(entityName, true, this.getClass().getClassLoader())
371           
372            // Initialize some session variables
373            //flow.importer_workbook = wb // workbook object must be serialized for this to work           
374             
375                flow.importer_template_id = params.template_id
376                flow.importer_sheetindex = params.sheetindex.toInteger() -1 // 0 == first sheet
377                flow.importer_datamatrix_start = params.datamatrix_start.toInteger() -1 // 0 == first row
378                flow.importer_headerrow = params.headerrow.toInteger()
379
380                // Get the header from the Excel file using the arguments given in the first step of the wizard
381                flow.importer_header = ImporterService.getHeader(session.importer_workbook,
382                    flow.importer_sheetindex,
383                    flow.importer_headerrow,
384                    flow.importer_datamatrix_start,
385                    entityClass)
386
387                // Initialize 'selected entities', used to show entities above the columns
388                flow.importer_header.each {
389                    selectedentities.add([name:entityName, columnindex:it.key.toInteger()])
390                }
391               
392                flow.importer_selectedentities = selectedentities               
393
394                session.importer_datamatrix = ImporterService.getDatamatrix(
395                            session.importer_workbook, flow.importer_header,
396                            flow.importer_sheetindex,
397                            flow.importer_datamatrix_start,
398                            5)
399             
400                flow.importer_templates = Template.get(flow.importer_template_id)
401                flow.importer_allfieldtypes = "true"     
402            /*else {
403                render (template:"common/error",
404                    model:[error:"Wrong permissions: you are not allowed to write to the study you selected (${flow.importer_study})."])
405            }*/
406
407            return true
408        }
409    }
410
411    /**
412     * Handle the property mapping page.
413     *
414     * @param Map LocalAttributeMap (the flow scope)
415     * @param Map GrailsParameterMap (the flow parameters = form data)
416     * @returns boolean true if correctly validated, otherwise false
417     */
418    boolean propertiesPage (flow, params) {
419        // Find actual Template object from the chosen template name
420        def template = Template.get(flow.importer_template_id)
421
422        params.columnproperty.index.each { columnindex, property ->
423            // Create an actual class instance of the selected entity with the selected template
424            // This should be inside the closure because in some cases in the advanced importer, the fields can have different target entities
425            def entityClass = Class.forName(flow.importer_header[columnindex.toInteger()].entity.getName(), true, this.getClass().getClassLoader())
426            def entityObj = entityClass.newInstance(template:template)
427
428            // Store the selected property for this column into the column map for the ImporterService
429            flow.importer_header[columnindex.toInteger()].property = property
430
431            // Look up the template field type of the target TemplateField and store it also in the map
432            flow.importer_header[columnindex.toInteger()].templatefieldtype = entityObj.giveFieldType(property)
433
434            // Is a "Don't import" property assigned to the column?
435            flow.importer_header[columnindex.toInteger()].dontimport = (property=="dontimport") ? true : false
436
437            //if it's an identifier set the mapping column true or false
438            entityObj.giveFields().each {
439                (it.preferredIdentifier && (it.name==property)) ? flow.importer_header[columnindex.toInteger()].identifier = true : false
440            }
441        }
442
443        // Import the workbook and store the table with entity records and store the failed cells
444        def (table, failedcells) = ImporterService.importData(flow.importer_template_id,
445                                                              session.importer_workbook,
446                                                              flow.importer_sheetindex,
447                                                              flow.importer_datamatrix_start,
448                                                              flow.importer_header)
449
450        flow.importer_importeddata = table       
451        flow.importer_failedcells = failedcells
452        return true
453    }
454
455    /**
456     * Handle the mapping page.
457     *
458     * @param Map LocalAttributeMap (the flow scope)
459     * @param Map GrailsParameterMap (the flow parameters = form data)
460     * @returns boolean true if correctly validated, otherwise false
461     */
462    boolean mappingsPage(flow, flash, params) {
463        flash.wizardErrors = [:]
464        flow.importer_invalidentities = 0       
465
466        flow.importer_importeddata.each { table ->
467            table.each { entity ->               
468                def invalidontologies = 0
469
470                // Set the fields for this entity by retrieving values from the params
471                entity.giveFields().each { field ->                   
472                        // field of type ontology and value "#invalidterm"?
473                        if (field.type == dbnp.studycapturing.TemplateFieldType.ONTOLOGYTERM &&
474                            params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] == "#invalidterm"
475                        ) {
476                            invalidontologies++
477                        } else
478                        if (field.type == dbnp.studycapturing.TemplateFieldType.ONTOLOGYTERM &&
479                            params["entity_" + entity.getIdentifier() + "_" + field.escapedName()] != "#invalidterm") {
480                            removeFailedCell(flow.importer_failedcells, entity)
481                            entity.setFieldValue (field.toString(), params["entity_" + entity.getIdentifier() + "_" + field.escapedName()])
482                        }
483                        else
484                            entity.setFieldValue (field.toString(), params["entity_" + entity.getIdentifier() + "_" + field.escapedName()])
485                }
486
487                // Determine entity class and add a parent (defined as Study in first step of wizard)
488                switch (entity.getClass()) {
489                    case [Subject, Sample, Event]:   entity.parent = flow.importer_study
490                }
491
492                // Try to validate the entity now all fields have been set
493                if (!entity.validate() || invalidontologies) {
494                    flow.importer_invalidentities++
495
496                    // add errors to map                   
497                    this.appendErrors(entity, flash.wizardErrors, "entity_"+entity.getIdentifier() + "_")
498                   
499                                        entity.errors.getAllErrors().each() {
500                                                log.error ".import wizard imported validation error:" + it
501                                        }
502                } else {                   
503                    removeFailedCell(flow.importer_failedcells, entity)
504                } // end else if
505
506            } // end of record
507        } // end of table
508
509        return (flow.importer_invalidentities == 0) ? true : false
510    } // end of method
511
512    /**
513     * @param failedcell failed ontology cells
514     * @param entity entity to remove from the failedcells list
515     */
516    def removeFailedCell(failedcells, entity) {       
517        // Valid entity, remove it from failedcells
518        failedcells.each { record ->
519            def tempimportcells = []
520           
521            record.importcells.each { cell ->           
522            // remove the cell from the failed cells session
523                if (cell.entityidentifier != entity.getIdentifier()) {
524                    //record.removeFromImportcells(cell)
525                    tempimportcells.add(cell)
526                }
527            }
528
529            record.importcells = tempimportcells
530           // } // end of importcells
531        } // end of failedcells
532    }
533
534    /**
535     * Handle the imported entities page.
536     *
537     * @param Map LocalAttributeMap (the flow scope)
538     * @param Map GrailsParameterMap (the flow parameters = form data)
539     * @returns boolean true if correctly validated, otherwise false
540     */
541    boolean importedPage(flow, params) {
542        return true
543    }
544
545    boolean saveEntities(flow, params) {
546            def (validatedSuccesfully, updatedEntities, failedToPersist) = ImporterService.saveDatamatrix(flow.importer_study, flow.importer_importeddata)
547
548            flow.importer_validatedsuccesfully = validatedSuccesfully
549            flow.importer_failedtopersist = failedToPersist
550            flow.imported_updatedentities = updatedEntities
551            flow.importer_totalrows = flow.importer_importeddata.size
552            flow.importer_referer = ""
553
554            return true
555    }
556
557    /**
558         * append errors of a particular object to a map
559         * @param object
560         * @param map linkedHashMap
561         * @void
562         */
563        def appendErrors(object, map) {
564                this.appendErrorMap(getHumanReadableErrors(object), map)
565    }
566
567        def appendErrors(object, map, prepend) {
568                this.appendErrorMap(getHumanReadableErrors(object), map, prepend)
569        }
570
571    /**
572         * append errors of one map to another map
573         * @param map linkedHashMap
574         * @param map linkedHashMap
575         * @void
576         */
577        def appendErrorMap(map, mapToExtend) {
578                map.each() {key, value ->
579                        mapToExtend[key] = ['key': key, 'value': value, 'dynamic': false]           
580                }
581        }
582
583        def appendErrorMap(map, mapToExtend, prepend) {
584                map.each() {key, value ->
585                        mapToExtend[prepend + key] = ['key': key, 'value': value, 'dynamic': true]
586                }
587        }
588
589    /**
590         * transform domain class validation errors into a human readable
591         * linked hash map
592         * @param object validated domain class
593         * @return object  linkedHashMap
594         */
595        def getHumanReadableErrors(object) {
596                def errors = [:]
597                object.errors.getAllErrors().each() { error ->
598                        // error.codes.each() { code -> println code }
599            println "errors is " + error
600
601                        // generally speaking g.message(...) should work,
602                        // however it fails in some steps of the wizard
603                        // (add event, add assay, etc) so g is not always
604                        // availably. Using our own instance of the
605                        // validationTagLib instead so it is always
606                        // available to us
607                        errors[ error.getArguments()[0] ] = validationTagLib.message(error: error)
608                }
609
610                return errors
611        }
612}
Note: See TracBrowser for help on using the repository browser.