source: trunk/grails-app/controllers/dbnp/studycapturing/WizardController.groovy @ 503

Last change on this file since 503 was 503, checked in by duh, 11 years ago
  • finished off grouping page
  • Property svn:keywords set to Date Author Rev
File size: 21.0 KB
Line 
1package dbnp.studycapturing
2
3import dbnp.data.*
4
5// Grails convertors is imported in order to create JSON objects
6import grails.converters.*
7
8
9/**
10 * Wizard Controler
11 *
12 * The wizard controller handles the handeling of pages and data flow
13 * through the study capturing wizard.
14 *
15 * TODO: refactor the 'handle*' methods to work as subflows instead
16 *               of methods outside of the flow
17 *
18 * @author Jeroen Wesbeek
19 * @since 20100107
20 * @package studycapturing
21 *
22 * Revision information:
23 * $Rev: 503 $
24 * $Author: duh $
25 * $Date: 2010-05-31 14:45:10 +0000 (ma, 31 mei 2010) $
26 */
27class WizardController {
28        /**
29         * index method, redirect to the webflow
30         * @void
31         */
32        def index = {
33                /**
34                 * Do you believe it in your head?
35                 * I can go with the flow
36                 * Don't say it doesn't matter (with the flow) matter anymore
37                 * I can go with the flow (I can go)
38                 * Do you believe it in your head?
39                 */
40                redirect(action: 'pages')
41        }
42
43        /**
44         * WebFlow definition
45         * @see http://grails.org/WebFlow
46         * @void
47         */
48        def pagesFlow = {
49                // start the flow
50                onStart {
51                        // define flow variables
52                        flow.page = 0
53                        flow.pages = [
54                                //[title: 'Templates'],                 // templates
55                                [title: 'Start'],                               // load or create a study
56                                [title: 'Study'],                               // study
57                                [title: 'Subjects'],                    // subjects
58                                [title: 'Events'],                              // events and event grouping
59                                [title: 'Groups'],                              // groups
60                                [title: 'Samples'],                             // samples
61                                [title: 'Confirmation'],                // confirmation page
62                                [title: 'Done']                                 // finish page
63                        ]
64                }
65
66                // render the main wizard page which immediately
67                // triggers the 'next' action (hence, the main
68                // page dynamically renders the study template
69                // and makes the flow jump to the study logic)
70                mainPage {
71                        render(view: "/wizard/index")
72                        onRender {
73                                flow.page = 1
74                        }
75                        on("next").to "start"
76                }
77
78                // create or modify a study
79                start {
80                        render(view: "_start")
81                        onRender {
82                                flow.page = 1
83                        }
84                        on("next").to "study"
85                        on("modify").to "modify"
86                }
87
88                // load a study to modify
89                modify {
90                        render(view: "_modify")
91                        onRender {
92                                flow.page = 1
93                                flash.cancel = true
94                        }
95                        on("cancel") {
96                                flow.study = null
97                        }.to "start"
98                        on("next") {
99                                // TODO: loading a study is not yet implemented
100                                //       create a error stating this feature is
101                                //       not yet implemented
102                                flash.errors = [:]
103                                this.appendErrorMap(
104                                        ['study': 'Loading a study and modifying it has not yet been implemented. Please press \'cancel\' to go back to the initial page...'],
105                                        flash.errors
106                                )
107                        }.to "modify"
108                }
109
110                // render and handle the study page
111                // TODO: make sure both template as well as logic will
112                //       handle Study templates as well!!!
113                study {
114                        render(view: "_study")
115                        onRender {
116                                flow.page = 2
117                        }
118                        on("refresh") {
119                                flash.values = params
120
121                                // handle study data
122                                this.handleStudy(flow, flash, params)
123
124                                // remove errors as we don't want any warnings now
125                                flash.errors = [:]                             
126                        }.to "study"
127                        on("switchTemplate") {
128                                flash.values = params
129
130                                // handle study data
131                                this.handleStudy(flow, flash, params)
132
133                                // remove errors as we don't want any warnings now
134                                flash.errors = [:]
135                        }.to "study"
136                        on("previous") {
137                                flash.errors = [:]
138
139                                // handle the study
140                                this.handleStudy(flow, flash, params)
141
142                                // reset errors
143                                flash.errors = [:]
144
145                                success()
146                        }.to "start"
147                        on("next") {
148                                flash.errors = [:]
149
150                                if (this.handleStudy(flow, flash, params)) {
151                                        success()
152                                } else {
153                                        error()
154                                }
155                        }.to "subjects"
156                }
157
158                // render and handle subjects page
159                subjects {
160                        render(view: "_subjects")
161                        onRender {
162                                flow.page = 3
163
164                                if (!flow.subjects) {
165                                        flow.subjects = [:]
166                                        flow.subjectTemplates = [:]
167                                }
168                        }
169                        on("refresh") {
170                                flash.values = params
171                        }.to "subjects"
172                        on("add") {
173                                // handle subjects
174                                this.handleSubjects(flow, flash, params)
175
176                                flash.errors = [:]
177                                flash.values = params
178                                def speciesTerm = Term.findByName(params.species);
179                                def subjectTemplateName = params.get('template');
180                                def subjectTemplate = Template.findByName(subjectTemplateName);
181
182                                // add this subject template to the subject template array
183                                if (!flow.subjectTemplates[ subjectTemplateName ]) {
184                                        flow.subjectTemplates[ subjectTemplateName ] = [
185                                                name: subjectTemplateName,
186                                                template: subjectTemplate,
187                                                subjects: [:]
188                                        ]
189                                }
190
191                                // add x subjects of species y
192                                (params.addNumber as int).times {
193                                        def increment = (flow.subjects.size()) ? (flow.subjects.keySet().max() + 1) : 0
194                                        def subject = new Subject(
195                                                name: 'Subject ' + (increment + 1),
196                                                species: speciesTerm,
197                                                template: subjectTemplate
198                                        )
199
200                                        // instantiate a new Subject
201                                        flow.subjects[ increment ] = subject
202
203                                        // and remember the subject id with the template
204                                        def subjectsSize = (flow.subjectTemplates[ subjectTemplateName ].subjects.size()) ? (flow.subjectTemplates[ subjectTemplateName ].subjects.keySet().max() + 1) : 0
205                                        flow.subjectTemplates[ subjectTemplateName ].subjects[ subjectsSize ] = increment
206                                }
207                        }.to "subjects"
208                        on("next") {
209                                flash.errors = [:]
210
211                                // check if we have at least one subject
212                                // and check form data
213                                if (flow.subjects.size() < 1) {
214                                        // append error map
215                                        this.appendErrorMap(['subjects': 'You need at least to create one subject for your study'], flash.errors)
216                                        error()
217                                } else if (!this.handleSubjects(flow, flash, params)) {
218                                        error()
219                                } else {
220                                        success()
221                                }
222                        }.to "events"
223                        on("delete") {
224                                // handle subjects
225                                this.handleSubjects(flow, flash, params)
226
227                                flash.errors = [:]
228                                def delete = params.get('do') as int;
229
230                                // remove subject
231                                if (flow.subjects[ delete ] && flow.subjects[ delete ] instanceof Subject) {
232                                        // remove subject from templates
233                                        flow.subjectTemplates.each() { templateName, templateData ->
234                                                templateData.subjects.remove( delete )
235                                        }
236
237                                        // remove subject altogether
238                                        flow.subjects.remove( delete )
239                                }
240                        }.to "subjects"
241                        on("previous") {
242                                flash.errors = [:]
243
244                                // handle form data
245                                if (!this.handleSubjects(flow, flash, params)) {
246                                        error()
247                                } else {
248                                        success()
249                                }
250                        }.to "study"
251                }
252
253                // render events page
254                events {
255                        render(view: "_events")
256                        onRender {
257                                flow.page = 4
258
259                                if (!flow.event) {
260                                        flow.event                      = new Event()
261                                        flow.events                     = []
262                                        flow.eventGroups        = []
263                                        flow.eventGroups[0]     = new EventGroup(name: 'Group 1')       // 1 group by default
264                                        flow.eventTemplates     = [:]
265                                } else if (!flash.values) {
266                                        // set flash.values.templateType based on the event instance
267                                        flash.values = [:]
268                                        flash.values.templateType = (flow.event instanceof Event) ? 'event' : 'sample'
269                                }
270                        }
271                        on("switchTemplate") {
272                                flash.values = params
273
274                                // handle study data
275                                this.handleEvents(flow, flash, params)
276
277                                // remove errors as we don't want any warnings now
278                                flash.errors = [:]
279                        }.to "events"
280                        on("add") {
281                                flash.values                    = params
282                                def eventTemplateName   = (params.get('eventType') == 'event') ? params.get('eventTemplate') : params.get('sampleTemplate')
283                                def eventTemplate               = Template.findByName(eventTemplateName)
284
285                                // handle study data
286                                this.handleEvents(flow, flash, params)
287
288                                // validate event object
289                                if (flow.event.validate()) {
290                                        // add this event template to the event template array
291                                        if (!flow.eventTemplates[ eventTemplateName ]) {
292                                                flow.eventTemplates[ eventTemplateName ] = [
293                                                        name: eventTemplateName,
294                                                        template: eventTemplate,
295                                                        events: []
296                                                ]
297                                        }
298
299                                        // it validated! Duplicate the event object...
300                                        def newEvent    = flow.event
301                                        def increment   = flow.events.size()
302
303                                        // ...store it in the events map in the flow scope...
304                                        flow.events[ increment ] = newEvent
305
306                                        // ...and 'reset' the event object in the flow scope
307                                        flow.event = new Event(template: newEvent.template)
308                                       
309                                        // remember the event id with the template
310                                        def eventSize = flow.eventTemplates[ eventTemplateName ]['events'].size()
311                                        flow.eventTemplates[ eventTemplateName ]['events'][ eventSize ] = increment
312
313                                        success()
314                                } else {
315                                        // it does not validate, show error feedback
316                                        flash.errors = [:]
317                                        this.appendErrors(flow.event, flash.errors)
318                                        error()
319                                }
320                        }.to "events"
321                        on("deleteEvent") {
322                                flash.values = params
323                                def delete = params.get('do') as int;
324
325                                // handle event groupings
326                                this.handleEventGrouping(flow, flash, params)
327
328                                // remove event
329                                if (flow.events[ delete ] && flow.events[ delete ] instanceof Event) {
330                                        flow.events.remove(delete)
331                                }
332                        }.to "events"
333                        on("addEventGroup") {
334                                flash.values = params
335                               
336                                // handle event groupings
337                                this.handleEventGrouping(flow, flash, params)
338
339                                def increment = flow.eventGroups.size()
340                                def groupName = "Group " + (increment + 1)
341
342                                // check if group name exists
343                                def nameExists = true
344                                def u = 0
345
346                                // make sure a unique name is generated
347                                while (nameExists) {
348                                        u++
349                                        def count = 0
350                                       
351                                        flow.eventGroups.each() {
352                                                if (it.name == groupName) {
353                                                        groupName = "Group " + (increment + 1) + "," + u
354                                                } else {
355                                                        count++
356                                                }
357                                        }
358
359                                        nameExists = !(count == flow.eventGroups.size())
360                                }
361
362                                flow.eventGroups[increment] = new EventGroup( name: groupName )
363                        }.to "events"
364                        on("deleteEventGroup") {
365                                flash.values = params
366
367                                def delete = params.get('do') as int;
368
369                                // handle event groupings
370                                this.handleEventGrouping(flow, flash, params)
371
372                                // remove the group with this specific id
373                                if (flow.eventGroups[delete] && flow.eventGroups[delete] instanceof EventGroup) {
374                                        // remove this eventGroup
375                                        flow.eventGroups.remove(delete)
376                                }
377                        }.to "events"
378                        on("previous") {
379                                // handle event groupings
380                                this.handleEventGrouping(flow, flash, params)
381                        }.to "subjects"
382                        on("next") {
383                                flash.values = params
384                                flash.errors = [:]
385
386                                // handle study data
387                                if (flow.events.size() < 1) {
388                                        // append error map
389                                        this.appendErrorMap(['events': 'You need at least to create one event for your study'], flash.errors)
390                                        error()                                         
391                                } else if (this.handleEvents(flow, flash, params)) {
392                                        success()
393                                } else {
394                                        error()
395                                }
396                        }.to "groups"
397                }
398
399                // groups page
400                groups {
401                        render(view: "_groups")
402                        onRender {
403                                flow.page = 5
404                        }
405                        on("previous") {
406                                this.handleSubjectGrouping(flow, flash, params)
407                        }.to "events"
408                        on("next") {
409                                this.handleSubjectGrouping(flow, flash, params)
410                        }.to "samples"
411                }
412
413                // samples page
414                samples {
415                        render(view: "_samples")
416                        onRender {
417                                flow.page = 6
418                        }
419                        on("previous") {
420                        }.to "groups"
421                        on("next") {
422                        }.to "samples"
423                }
424
425                // confirmation
426                confirm {
427                        render(view: "_confirmation")
428                        onRender {
429                                flow.page = 7
430                        }
431                        on("toStudy").to "study"
432                        on("toSubjects").to "subjects"
433                        on("toEvents").to "events"
434                        on("toGroups").to "groups"
435                        on("previous").to "groups"
436                        on("next").to "save"
437                }
438
439                // store all study data
440                save {
441                        action {
442                                println "saving..."
443                                flash.errors = [:]
444
445                                // start transaction
446                                def transaction = sessionFactory.getCurrentSession().beginTransaction()
447
448                                // persist data to the database
449                                try {
450                                        // save EventDescriptions
451                                        flow.eventDescriptions.each() {
452                                                if (!it.save(flush:true)) {
453                                                        this.appendErrors(it, flash.errors)
454                                                        throw new Exception('error saving eventDescription')
455                                                }
456                                                println "saved eventdescription "+it
457                                        }
458
459                                        // TODO: eventDescriptions that are not linked to an event are currently
460                                        //               stored but end up in a black hole. We should either decide to
461                                        //               NOT store these eventDescriptions, or add "hasmany eventDescriptions"
462                                        //               to Study domain class
463
464                                        // save events
465                                        flow.events.each() {
466                                                if (!it.save(flush:true)) {
467                                                        this.appendErrors(it, flash.errors)
468                                                        throw new Exception('error saving event')
469                                                }
470                                                println "saved event "+it
471
472                                                // add to study
473                                                if (it instanceof SamplingEvent) {
474                                                        flow.study.addToSamplingEvents(it)
475                                                } else {
476                                                        flow.study.addToEvents(it)
477                                                }
478                                        }
479
480                                        // save eventGroups
481                                        flow.eventGroups.each() {
482                                                if (!it.save(flush:true)) {
483                                                        this.appendErrors(it, flash.errors)
484                                                        throw new Exception('error saving eventGroup')
485                                                }
486                                                println "saved eventGroup "+it
487
488                                                // add to study
489                                                flow.study.addToEventGroups(it)
490                                        }
491                                       
492                                        // save subjects
493                                        flow.subjects.each() {
494                                                if (!it.save(flush:true)) {
495                                                        this.appendErrors(it, flash.errors)
496                                                        throw new Exception('error saving subject')
497                                                }
498                                                println "saved subject "+it
499
500                                                // add this subject to the study
501                                                flow.study.addToSubjects(it)
502                                        }
503
504                                        // save study
505                                        if (!flow.study.save(flush:true)) {
506                                                this.appendErrors(flow.study, flash.errors)
507                                                throw new Exception('error saving study')
508                                        }
509                                        println "saved study "+flow.study+" (id: "+flow.study.id+")"
510
511                                        // commit transaction
512                                        println "commit"
513                                        transaction.commit()
514                                        success()
515                                } catch (Exception e) {
516                                        // rollback
517                                        this.appendErrorMap(['exception': e.toString() + ', see log for stacktrace' ], flash.errors)
518
519                                        // stacktrace in flash scope
520                                        flash.debug = e.getStackTrace()
521
522                                        println "rollback"
523                                        transaction.rollback()
524                                        error()
525                                }
526                        }
527                        on("error").to "error"
528                        on(Exception).to "error"
529                        on("success").to "done"
530                }
531
532                // error storing data
533                error {
534                        render(view: "_error")
535                        onRender {
536                                flow.page = 7
537                        }
538                        on("next").to "save"
539                        on("previous").to "samples"
540                }
541
542                // render page three
543                done {
544                        render(view: "_done")
545                        onRender {
546                                flow.page = 8
547                        }
548                        on("previous") {
549                                // TODO
550                        }.to "confirm"
551                }
552        }
553
554        /**
555         * re-usable code for handling study form data in a web flow
556         * @param Map LocalAttributeMap (the flow scope)
557         * @param Map localAttributeMap (the flash scope)
558         * @param Map GrailsParameterMap (the flow parameters = form data)
559         * @returns boolean
560         */
561        def handleStudy(flow, flash, params) {
562                // create study instance if we have none
563                if (!flow.study) flow.study = new Study();
564
565                // create date instance from date string?
566                // @see WizardTagLibrary::dateElement{...}
567                if (params.get('startDate')) {
568                        params.startDate = new Date().parse("d/M/yyyy", params.get('startDate').toString())
569                } else {
570                        params.remove('startDate')
571                }
572
573                // if a template is selected, get template instance
574                def template = params.remove('template')
575                if (template instanceof String && template.size() > 0) {
576                        flow.study.template = Template.findByName(template)
577                } else if (template instanceof Template) {
578                        flow.study.template = template
579                }
580
581                // iterate through fields
582                if (flow.study.template) {
583                        flow.study.giveFields().each() {
584                                flow.study.setFieldValue(it.name, params.get(it.escapedName()))
585                        }
586                }
587
588                // validate study
589                if (flow.study.validate()) {
590                        return true
591                } else {
592                        // validation failed, feedback errors
593                        flash.errors = [:]
594                        this.appendErrors(flow.study, flash.errors)
595                        return false
596                }
597        }
598
599        /**
600         * re-usable code for handling subject form data in a web flow
601         * @param Map LocalAttributeMap (the flow scope)
602         * @param Map localAttributeMap (the flash scope)
603         * @param Map GrailsParameterMap (the flow parameters = form data)
604         * @returns boolean
605         */
606        def handleSubjects(flow, flash, params) {
607                def names = [:];
608                def errors = false;
609                def id = 0;
610
611                // iterate through subject templates
612                flow.subjectTemplates.each() { subjectTemplate ->
613                        // iterate through subjects
614                        subjectTemplate.getValue().subjects.each() { subjectIncrement, subjectId ->
615                                // iterate through fields (= template fields and domain properties)
616                                flow.subjects[ subjectId ].giveFields().each() { subjectField ->
617                                        // set the field
618                                        flow.subjects[ subjectId ].setFieldValue(
619                                                subjectField.name,
620                                                params.get( 'subject_' + subjectId + '_' + subjectField.escapedName() )
621                                        )
622                                }
623
624                                // validate subject
625                                if (!flow.subjects[ subjectId ].validate()) {
626                                        errors = true
627                                        this.appendErrors(flow.subjects[ subjectId ], flash.errors, 'subject_' + subjectId + '_')
628                                }
629                        }
630                }
631
632                return !errors
633        }
634
635        /**
636         * re-usable code for handling event form data in a web flow
637         * @param Map LocalAttributeMap (the flow scope)
638         * @param Map localAttributeMap (the flash scope)
639         * @param Map GrailsParameterMap (the flow parameters = form data)
640         * @returns boolean
641         */
642        def handleEvents(flow, flash, params) {
643                def errors = false
644                def template = null
645
646                // handle the type of event
647                if (params.eventType == 'event') {
648                        flow.event = new Event();
649                        template = params.remove('eventTemplate')
650                } else if (params.eventType == 'sample') {
651                        flow.event = new SamplingEvent();
652                        template = params.remove('sampleTemplate')
653                }
654
655                // got an event in the flow scope?
656                //if (!flow.event) flow.event = new Event()
657
658                // if a template is selected, get template instance
659                if (template instanceof String && template.size() > 0) {
660                        params.template = Template.findByName(template)
661                } else if (template instanceof Template) {
662                        params.template = template
663                } else {
664                        params.template = null
665                }
666
667                // set template
668                if (params.template) flow.event.template = params.template
669
670                // update event instance with parameters
671                flow.event.giveFields().each() { eventField ->
672                        flow.event.setFieldValue(eventField.name, params[ eventField.escapedName() ])   
673                }
674
675                // handle event objects
676                flow.eventTemplates.each() { eventTemplate ->
677                        // iterate through events
678                        eventTemplate.getValue().events.each() { eventId ->
679                                // iterate through template fields
680                                flow.events[ eventId ].giveFields().each() { eventField ->
681                                        flow.events[ eventId ].setFieldValue(eventField.name, params.get( 'event_' + eventId + '_' + eventField.escapedName() ) )
682                                }
683
684                                // validate event
685                                if (!flow.events[ eventId ].validate()) {
686                                        errors = true
687                                        this.appendErrors(flow.events[ eventId ], flash.errors, 'event_' + eventId + '_')
688                                }
689                        }
690                }
691
692                // handle event grouping
693                handleEventGrouping(flow, flash, params)
694
695                return !errors
696        }
697
698        /**
699         * re-usable code for handling event grouping in a web flow
700         * @param Map LocalAttributeMap (the flow scope)
701         * @param Map localAttributeMap (the flash scope)
702         * @param Map GrailsParameterMap (the flow parameters = form data)
703         * @returns boolean
704         */
705        def handleEventGrouping(flow, flash, params) {
706                // walk through eventGroups
707                def g = 0
708                flow.eventGroups.each() { eventGroup ->
709                        def e = 0
710
711                        // reset events
712                        eventGroup.events = new HashSet()
713
714                        // iterate through events
715                        flow.events.each() {
716                                if (params.get('event_' + e + '_group_' + g) == 'on') {
717                                        eventGroup.addToEvents(it)
718                                }
719                                e++
720                        }
721                        g++
722                }
723        }
724
725        /**
726         * re-usable code for handling subject grouping in a web flow
727         * @param Map LocalAttributeMap (the flow scope)
728         * @param Map localAttributeMap (the flash scope)
729         * @param Map GrailsParameterMap (the flow parameters = form data)
730         * @returns boolean
731         */
732        def handleSubjectGrouping(flow, flash, params) {
733                println params
734
735                // iterate through event groups
736                def g = 0
737                flow.eventGroups.each() { eventGroup ->
738                        // reset subjects
739                        eventGroup.subjects = new HashSet()
740
741                        // iterate through subjects
742                        flow.subjects.each() { subjectId, subject ->
743                                // is this combination set?
744                                if (params.get('subject_' + subjectId + '_group_' + g) != null) {
745                                        eventGroup.addToSubjects(subject)
746                                }
747                        }
748
749                        println g+" : "+eventGroup.subjects
750
751                        g++
752                }
753        }
754
755        /**
756         * return the object from a map of objects by searching for a name
757         * @param String name
758         * @param Map map of objects
759         * @return Object
760         */
761        def getObjectByName(name, map) {
762                def result = null
763                map.each() {
764                        if (it.name == name) {
765                                result = it
766                        }
767                }
768
769                return result
770        }
771
772        /**
773         * transform domain class validation errors into a human readable
774         * linked hash map
775         * @param object validated domain class
776         * @returns object  linkedHashMap
777         */
778        def getHumanReadableErrors(object) {
779                def errors = [:]
780                object.errors.getAllErrors().each() {
781                        def message = it.toString()
782
783                        //errors[it.getArguments()[0]] = it.getDefaultMessage()
784                        errors[it.getArguments()[0]] = message.substring(0, message.indexOf(';'))
785                }
786
787                return errors
788        }
789
790        /**
791         * append errors of a particular object to a map
792         * @param object
793         * @param map linkedHashMap
794         * @void
795         */
796        def appendErrors(object, map) {
797                this.appendErrorMap(this.getHumanReadableErrors(object), map)
798        }
799
800        def appendErrors(object, map, prepend) {
801                this.appendErrorMap(this.getHumanReadableErrors(object), map, prepend)
802        }
803
804        /**
805         * append errors of one map to another map
806         * @param map linkedHashMap
807         * @param map linkedHashMap
808         * @void
809         */
810        def appendErrorMap(map, mapToExtend) {
811                map.each() {key, value ->
812                        mapToExtend[key] = ['key': key, 'value': value, 'dynamic': false]
813                }
814        }
815
816        def appendErrorMap(map, mapToExtend, prepend) {
817                map.each() {key, value ->
818                        mapToExtend[prepend + key] = ['key': key, 'value': value, 'dynamic': true]
819                }
820        }
821
822        /**
823         * Parses a RelTime string and returns a nice human readable string
824         *
825         * @returns Human Readable string or a HTTP response code 400 on error
826         */
827        def ajaxParseRelTime = {
828                if (params.reltime == null) {
829                        response.status = 400;
830                        render('reltime parameter is expected');
831                }
832
833                try {
834                        def reltime = RelTime.parseRelTime(params.reltime);
835                        render reltime.toPrettyString();
836                } catch (IllegalArgumentException e) {
837                        response.status = 400;
838                        render(e.getMessage());
839                }
840        }
841}
Note: See TracBrowser for help on using the repository browser.