source: trunk/grails-app/controllers/dbnp/visualization/VisualizeController.groovy @ 1993

Last change on this file since 1993 was 1993, checked in by taco@…, 9 years ago

Improved visualization controller. Added some error handling.

File size: 26.2 KB
Line 
1/**
2 * Visualize Controller
3 *
4 * This controller enables the user to visualize his data
5 *
6 * @author  robert@thehyve.nl
7 * @since       20110825
8 * @package     dbnp.visualization
9 *
10 * Revision information:
11 * $Rev$
12 * $Author$
13 * $Date$
14 */
15package dbnp.visualization
16
17import dbnp.studycapturing.*;
18import grails.converters.JSON
19import groovy.lang.Closure;
20
21import org.dbnp.gdt.*
22
23class VisualizeController {
24        def authenticationService
25        def moduleCommunicationService
26
27        /**
28         * Shows the visualization screen
29         */
30        def index = {
31                [ studies: Study.giveReadableStudies( authenticationService.getLoggedInUser() )]
32        }
33
34        def getStudies = {
35                def studies = Study.giveReadableStudies( authenticationService.getLoggedInUser() );
36                render studies as JSON
37        }
38
39        def getFields = {
40                def input_object
41                def studies
42
43                try{
44                        input_object = JSON.parse(params.get('data'))
45                        studies = input_object.get('studies').id
46                } catch(Exception e) {
47                        returnError(400, "An error occured while retrieving the user input.")
48            log.error("VisualizationController: getFields: "+e)
49                }
50
51                def fields = [];
52                studies.each {
53                        /*
54                         Gather fields related to this study from GSCF.
55                         This requires:
56                         - a study.
57                         - a category variable, e.g. "events".
58                         - a type variable, either "domainfields" or "templatefields".
59                         */
60                        def study = Study.get(it)
61                        fields += getFields(study, "subjects", "domainfields")
62                        fields += getFields(study, "subjects", "templatefields")
63                        fields += getFields(study, "events", "domainfields")
64                        fields += getFields(study, "events", "templatefields")
65                        fields += getFields(study, "samplingEvents", "domainfields")
66                        fields += getFields(study, "samplingEvents", "templatefields")
67                        fields += getFields(study, "assays", "domainfields")
68                        fields += getFields(study, "assays", "templatefields")
69                        fields += getFields(study, "samples", "domainfields")
70                        fields += getFields(study, "samples", "domainfields")
71
72            /*
73            Gather fields related to this study from modules.
74            This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
75            It does not actually return measurements (the getMeasurementData call does).
76            The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement
77            types related to these assays.
78            So, the required variables for such a call are:
79              - a source variable, which can be obtained from AssayModule.list() (use the 'name' field)
80              - an assay, which can be obtained with study.getAssays()
81             */
82
83            study.getAssays().each { assay ->
84                def list = []
85                list = getFields(assay.module.id, assay)
86                if(list!=null){
87                    if(list.size()!=0){
88                        fields += list
89
90                    }
91                }
92            }
93
94                        // TODO: Maybe we should add study's own fields
95                }
96
97                render fields as JSON
98        }
99
100        def getVisualizationTypes = {
101                def types = [ [ "id": "barchart", "name": "Barchart"] ];
102                render types as JSON
103        }
104
105    def getFields(source, assay){
106        /*
107        Gather fields related to this study from modules.
108        This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
109        getMeasurements does not actually return measurements (the getMeasurementData call does).
110        The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement
111        types related to these assays.
112        So, the required variables for such a call are:
113          - a source variable, which can be obtained from AssayModule.list() (use the 'name' field)
114          - a list of assays, which can be obtained with study.getAssays()
115
116        Output is a list of items. Each item contains
117          - an 'id'
118          - a 'source', which is a module identifier
119          - a 'category', which indicates where the field can be found. When dealing with module data as we are here, this is the assay name(s)
120          - a 'name', which is the the name of the field
121         */
122        def fields = []
123        def callUrl = ""
124
125        // Making a different call for each assay
126        // TODO: Change this to one call that requests fields for all assays, when you get that to work (in all cases)
127
128        def urlVars = "assayToken="+assay.assayUUID
129        try {
130            callUrl = ""+assay.module.url + "/rest/getMeasurements/query?"+urlVars
131            def json = moduleCommunicationService.callModuleRestMethodJSON( assay.module.url /* consumer */, callUrl );
132            def collection = []
133            json.each{ jason ->
134                collection.add(jason)
135            }
136            // Formatting the data
137            collection.each { field ->
138                // For getting this field from this assay
139                fields << [ "id": createFieldId( id: field, name: field, source: assay.id, type: ""+assay.name), "source": source, "category": ""+assay.name, "name": field ]
140            }
141        } catch(Exception e){
142            returnError(404, "An error occured while trying to collect field data from a module. Most likely, this module is offline.")
143            log.error("VisualizationController: getFields: "+e)
144        }
145
146        return fields
147    }
148
149   def getFields(study, category, type){
150        /*
151        Gather fields related to this study from GSCF.
152        This requires:
153          - a study.
154          - a category variable, e.g. "events".
155          - a type variable, either "domainfields" or "templatefields".
156
157        Output is a list of items, which are formatted by the formatGSCFFields function.
158        */
159
160        // Collecting the data from it's source
161        def collection
162        def fields = []
163        def source = "GSCF"
164
165        // Gathering the data
166        if(category=="subjects"){
167            if(type=="domainfields"){
168                collection = Subject.giveDomainFields()
169            }
170            if(type=="templatefields"){
171                collection = study?.samples?.parentSubject?.template?.fields
172            }
173        }
174        if(category=="events"){
175            if(type=="domainfields"){
176                collection = Event.giveDomainFields()
177            }
178            if(type=="templatefields"){
179                collection = study?.samples?.parentEventGroup?.events?.template?.fields
180            }
181        }
182        if(category=="samplingEvents"){
183            if(type=="domainfields"){
184                collection = SamplingEvent.giveDomainFields()
185            }
186            if(type=="templatefields"){
187                collection = study?.samples?.parentEventGroup?.samplingEvents?.template?.fields
188            }
189        }
190        if(category=="samples"){
191            if(type=="domainfields"){
192                collection = Sample.giveDomainFields()
193            }
194            if(type=="templatefields"){
195                collection = study?.samples?.template?.fields
196            }
197        }
198        if(category=="assays"){
199            if(type=="domainfields"){
200                collection = Assay.giveDomainFields()
201            }
202            if(type=="templatefields"){
203                collection = study?.assays?.template?.fields
204            }
205        }
206
207        collection.unique()
208
209        // Formatting the data
210        fields += formatGSCFFields(type, collection, source, category)
211
212        return fields
213    }
214
215    def formatGSCFFields(type, inputObject, source, category){
216        /*  The formatGSCFFields function can receive both lists of fields and single fields. If it receives a list, it calls itself again for each item in the list. This way, the original formatGSCFFields call will return single fields regardless of it's input.
217
218        Output is a list of items. Each item contains
219          - an 'id'
220          - a 'source', which in this case will be "GSCF"
221          - a 'category', which indicates where the field can be found, e.g. "subjects", "samplingEvents"
222          - a 'name', which is the the name of the field
223         */
224        if(inputObject==null || inputObject == []){
225            return []
226        }
227        def fields = []
228        if(inputObject instanceof Collection){
229            // Apparently this field is actually a list of fields.
230            // We will call ourselves again with the list's elements as input.
231            // These list elements will themselves go through this check again, effectively flattening the original input
232            for(int i = 0; i < inputObject.size(); i++){
233                fields += formatGSCFFields(type, inputObject[i], source, category)
234            }
235            return fields
236        } else {
237            // This is a single field. Format it and return the result.
238            if(type=="domainfields"){
239                fields << [ "id": createFieldId( id: inputObject.name, name: inputObject.name, source: source, type: category ), "source": source, "category": category, "name": inputObject.name ]
240            }
241            if(type=="templatefields"){
242                fields << [ "id": createFieldId( id: inputObject.id, name: inputObject.name, source: source, type: category ), "source": source, "category": category, "name": inputObject.name ]
243            }
244            return fields
245        }
246    }
247
248        /**
249         * Retrieves data for the visualization itself.
250         */
251        def getData = {
252                // Extract parameters
253                // TODO: handle erroneous input data
254                def inputData = parseGetDataParams();
255               
256                // TODO: handle the case that we have multiple studies
257                def studyId = inputData.studyIds[ 0 ];
258                def study = Study.get( studyId as Integer );
259
260                // Find out what samples are involved
261                def samples = study.samples
262
263                // Retrieve the data for both axes for all samples
264                // TODO: handle the case of multiple fields on an axis
265                def fields = [ "x": inputData.columnIds[ 0 ], "y": inputData.rowIds[ 0 ] ];
266                def data = getAllFieldData( study, samples, fields );
267
268                // Group data based on the y-axis if categorical axis is selected
269                // TODO: handle categories and continuous data
270                def groupedData = groupFieldData( data );
271
272                // Format data so it can be rendered as JSON
273                def returnData = formatData( groupedData, fields );
274
275                render returnData as JSON
276        }
277
278        /**
279         * Parses the parameters given by the user into a proper list
280         * @return Map with 4 keys:
281         *              studyIds:       list with studyIds selected
282         *              rowIds:         list with fieldIds selected for the rows
283         *              columnIds:      list with fieldIds selected for the columns
284         *              visualizationType:      String with the type of visualization required
285         * @see getFields
286         * @see getVisualizationTypes
287         */
288        def parseGetDataParams() {
289                def studyIds, rowIds, columnIds, visualizationType;
290               
291                def inputData = params.get( 'data' );
292                try{
293                        def input_object = JSON.parse(inputData)
294                       
295                        studyIds = input_object.get('studies')*.id
296                        rowIds = input_object.get('rows')*.id
297                        columnIds = input_object.get('columns')*.id
298                        visualizationType = "barchart"
299                } catch(Exception e) {
300                        returnError(400, "An error occured while retrieving the user input")
301                        log.error("VisualizationController: parseGetDataParams: "+e)
302                }
303
304                return [ "studyIds" : studyIds, "rowIds": rowIds, "columnIds": columnIds, "visualizationType": visualizationType ];
305        }
306
307        /**
308         * Retrieve the field data for the selected fields
309         * @param study         Study for which the data should be retrieved
310         * @param samples       Samples for which the data should be retrieved
311         * @param fields        Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
312         *                                              [ "x": "field-id-1", "y": "field-id-3" ]
313         * @return                      A map with the same keys as the input fields. The values in the map are lists of values of the
314         *                                      selected field for all samples. If a value could not be retrieved for a sample, null is returned. Example:
315         *                                              [ "x": [ 3, 6, null, 10 ], "y": [ "male", "male", "female", "female" ] ]
316         */
317        def getAllFieldData( study, samples, fields ) {
318                def fieldData = [:]
319                fields.each{ field ->
320                        fieldData[ field.key ] = getFieldData( study, samples, field.value );
321                }
322               
323                return fieldData;
324        }
325       
326        /**
327        * Retrieve the field data for the selected field
328        * @param study          Study for which the data should be retrieved
329        * @param samples        Samples for which the data should be retrieved
330        * @param fieldId        ID of the field to return data for
331        * @return                       A list of values of the selected field for all samples. If a value
332        *                                       could not be retrieved for a sample, null is returned. Examples:
333        *                                               [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
334        */
335        def getFieldData( study, samples, fieldId ) {
336                // Parse the fieldId as given by the user
337                def parsedField = parseFieldId( fieldId );
338               
339                def data = []
340               
341                if( parsedField.source == "GSCF" ) {
342                        // Retrieve data from GSCF itself
343                        def closure = valueCallback( parsedField.type )
344                       
345                        if( closure ) {
346                                samples.each { sample ->
347                                        // Retrieve the value for the selected field for this sample
348                                        def value = closure( sample, parsedField.name );
349                                       
350                                        if( value ) {
351                                                data << value;
352                                        } else {
353                                                // Return null if the value is not found
354                                                data << null
355                                        }
356                                }
357                        } else {
358                                // TODO: Handle error properly
359                                // Closure could not be retrieved, probably because the type is incorrect
360                                data = samples.collect { return null }
361                log.error("VisualizationController: getFieldData: Requested wrong field type: "+parsedField.type+". Parsed field: "+parsedField)
362                        }
363                } else {
364                        // Data must be retrieved from a module
365                        data = getModuleData( study, samples, parsedField.source, parsedField.name );
366                }
367               
368                return data
369        }
370       
371        /**
372         * Retrieve data for a given field from a data module
373         * @param study                 Study to retrieve data for
374         * @param samples               Samples to retrieve data for
375         * @param source_module Name of the module to retrieve data from
376         * @param fieldName             Name of the measurement type to retrieve (i.e. measurementToken)
377         * @return                              A list of values of the selected field for all samples. If a value
378         *                                              could not be retrieved for a sample, null is returned. Examples:
379         *                                                      [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
380         */
381        def getModuleData( study, samples, source_module, fieldName ) {
382                def data = []
383               
384                // TODO: Handle values that should be retrieved from multiple assays
385                def assay = Assay.get(source_module);
386               
387                if( assay ) {
388                        // Request for a particular assay and a particular feature
389                        def urlVars = "assayToken=" + assay.assayUUID + "&measurementToken="+fieldName
390                        urlVars += "&" + samples.collect { "sampleToken=" + it.sampleUUID }.join( "&" );
391                       
392                        def callUrl
393                        try {
394                                callUrl = assay.module.url + "/rest/getMeasurementData"
395                                def json = moduleCommunicationService.callModuleMethod( assay.module.url, callUrl, urlVars, "POST" );
396                               
397                                if( json ) {
398                                        // First element contains sampletokens
399                                        // Second element contains the featurename
400                                        // Third element contains the measurement value
401                                        def sampleTokens = json[ 0 ]
402                                        def measurements = json[ 2 ]
403                                       
404                                        // Loop through the samples
405                                        samples.each { sample ->
406                                                // Search for this sampletoken
407                                                def sampleToken = sample.sampleUUID;
408                                                def index = sampleTokens.findIndexOf { it == sampleToken }
409                                               
410                                                if( index > -1 ) {
411                                                        data << measurements[ index ];
412                                                } else {[ "x": inputData.columnIds[ 0 ], "y": inputData.rowIds[ 0 ] ]
413                                                        data << null
414                                                }
415                                        }
416                                } else {
417                                        // TODO: handle error
418                                        // Returns an empty list with as many elements as there are samples
419                                        data = samples.collect { return null }
420                                }
421                               
422                        } catch(Exception e){
423                returnError(404, "An error occured while trying to collect data from a module. Most likely, this module is offline.")
424                log.error("VisualizationController: getFields: "+e)
425                        }
426                } else {
427                        // TODO: Handle error correctly
428                        // Returns an empty list with as many elements as there are samples
429                        data = samples.collect { return null }
430                }
431               
432                return data
433
434        }
435
436        /**
437         * Group the field data on the values of the specified axis. For example, for a bar chart, the values
438         * on the x-axis should be grouped. Currently, the values for each group are averaged, and the standard
439         * error of the mean is returned in the 'error' property
440         * @param data          Data for both group- and value axis. The output of getAllFieldData fits this input
441         * @param groupAxis     Name of the axis to group on. Defaults to "x"
442         * @param valueAxis     Name of the axis where the values are. Defaults to "y"
443         * @param errorName     Key in the output map where 'error' values (SEM) are stored. Defaults to "error"
444         * @param unknownName   Name of the group for all null groups. Defaults to "unknown"
445         * @return                      A map with the keys 'groupAxis', 'valueAxis' and 'errorName'. The values in the map are lists of values of the
446         *                                      selected field for all groups. For example, if the input is
447         *                                              [ "x": [ "male", "male", "female", "female", null, "female" ], "y": [ 3, 6, null, 10, 4, 5 ] ]
448         *                                      the output will be:
449         *                                              [ "x": [ "male", "female", "unknown" ], "y": [ 4.5, 7.5, 4 ], "error": [ 1.5, 2.5, 0 ] ]
450         *
451         *                                      As you can see: null values in the valueAxis are ignored. Null values in the
452         *                                      group axis are combined into a 'unknown' category.
453         */
454        def groupFieldData( data, groupAxis = "x", valueAxis = "y", errorName = "error", unknownName = "unknown" ) {
455                // Create a unique list of values in the groupAxis. First flatten the list, since it might be that a
456                // sample belongs to multiple groups. In that case, the group names should not be the lists, but the list
457                // elements. A few lines below, this case is handled again by checking whether a specific sample belongs
458                // to this group.
459                // After flattening, the list is uniqued. The closure makes sure that values with different classes are
460                // always treated as different items (e.g. "" should not equal 0, but it does if using the default comparator)
461                def groups = data[ groupAxis ]
462                                                .flatten()
463                                                .unique { it == null ? "null" : it.class.name + it.toString() }
464               
465                // Make sure the null category is last
466                groups = groups.findAll { it != null } + groups.findAll { it == null }
467               
468                // Gather names for the groups. Most of the times, the group names are just the names, only with
469                // a null value, the unknownName must be used
470                def groupNames = groups.collect { it != null ? it : unknownName }
471               
472                // Generate the output object
473                def outputData = [:]
474                outputData[ valueAxis ] = [];
475                outputData[ errorName ] = [];
476                outputData[ groupAxis ] = groupNames;
477               
478                // Loop through all groups, and gather the values for this group
479                groups.each { group ->
480                        // Find the indices of the samples that belong to this group. if a sample belongs to multiple groups (i.e. if
481                        // the samples groupAxis contains multiple values, is a collection), the value should be used in all groups.
482                        def indices= data[ groupAxis ].findIndexValues { it instanceof Collection ? it.contains( group ) : it == group };
483                        def values = data[ valueAxis ][ indices ]
484                       
485                        def dataForGroup = computeMeanAndError( values );
486                       
487                        outputData[ valueAxis ] << dataForGroup.value
488                        outputData[ errorName ] << dataForGroup.error 
489                }
490
491                return outputData
492        }
493       
494        /**
495         * Formats the grouped data in such a way that the clientside visualization method
496         * can handle the data correctly.
497         * @param groupedData   Data that has been grouped using the groupFields method
498         * @param fields                Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
499         *                                                      [ "x": "field-id-1", "y": "field-id-3" ]
500         * @param groupAxis             Name of the axis to with group data. Defaults to "x"
501         * @param valueAxis             Name of the axis where the values are stored. Defaults to "y"
502         * @param errorName             Key in the output map where 'error' values (SEM) are stored. Defaults to "error"         *
503         * @return                              A map like the following:
504         *
505                        {
506                                "type": "barchart",
507                                "x": [ "Q1", "Q2", "Q3", "Q4" ],
508                                "xaxis": { "title": "quarter 2011", "unit": "" },
509                                "yaxis": { "title": "temperature", "unit": "degrees C" },
510                                "series": [
511                                        {
512                                                "name": "series name",
513                                                "y": [ 5.1, 3.1, 20.6, 15.4 ],
514                                                "error": [ 0.5, 0.2, 0.4, 0.5 ]
515                                        },
516                                ]
517                        }
518         *
519         */
520        def formatData( groupedData, fields, groupAxis = "x", valueAxis = "y", errorName = "error" ) {
521                // TODO: Handle name and unit of fields correctly
522               
523                def return_data = [:]
524                return_data[ "type" ] = "barchart"
525                return_data[ "x" ] = groupedData[ groupAxis ].collect { it.toString() }
526                return_data.put("yaxis", ["title" : parseFieldId( fields[ valueAxis ] ).name, "unit" : "" ])
527                return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": "" ])
528                return_data.put("series", [[
529                        "name": "Y",
530                        "y": groupedData[ valueAxis ],
531                        "error": groupedData[ errorName ]
532                ]])
533               
534                return return_data;
535        }
536
537        /**
538         * Returns a closure for the given entitytype that determines the value for a criterion
539         * on the given object. The closure receives two parameters: the sample and a field.
540         *
541         * For example:
542         *              How can one retrieve the value for subject.name, given a sample? This can be done by
543         *              returning the field values sample.parentSubject:
544         *                      { sample, field -> return getFieldValue( sample.parentSubject, field ) }
545         * @return      Closure that retrieves the value for a field and the given field
546         */
547        protected Closure valueCallback( String entity ) {
548                switch( entity ) {
549                        case "Study":
550                        case "studies":
551                                return { sample, field -> return getFieldValue( sample.parent, field ) }
552                        case "Subject":
553                        case "subjects":
554                                return { sample, field -> return getFieldValue( sample.parentSubject, field ); }
555                        case "Sample":
556                        case "samples":
557                                return { sample, field -> return getFieldValue( sample, field ) }
558                        case "Event":
559                        case "events":
560                                return { sample, field ->
561                                        if( !sample || !sample.parentEventGroup || !sample.parentEventGroup.events || sample.parentEventGroup.events.size() == 0 )
562                                                return null
563
564                                        return sample.parentEventGroup.events?.collect { getFieldValue( it, field ) };
565                                }
566                        case "SamplingEvent":
567                        case "samplingEvents":
568                                return { sample, field -> return getFieldValue( sample.parentEvent, field ); }
569                        case "Assay":
570                        case "assays":
571                                return { sample, field ->
572                                        def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) };
573                                        if( sampleAssays && sampleAssays.size() > 0 )
574                                                return sampleAssays.collect { getFieldValue( it, field ) }
575                                        else
576                                                return null
577                                }
578                }
579        }
580       
581        /**
582         * Computes the mean value and Standard Error of the mean (SEM) for the given values
583         * @param values        List of values to compute the mean and SEM for. Strings and null
584         *                                      values are ignored
585         * @return                      Map with two keys: 'value' and 'error'
586         */
587        protected Map computeMeanAndError( values ) {
588                // TODO: Handle the case that one of the values is a list. In that case,
589                // all values should be taken into account.     
590                def mean = computeMean( values );
591                def error = computeSEM( values, mean );
592               
593                return [ 
594                        "value": mean,
595                        "error": error
596                ]
597        }
598       
599        /**
600         * Computes the mean of the given values. Values that can not be parsed to a number
601         * are ignored. If no values are given, the mean of 0 is returned.
602         * @param values        List of values to compute the mean for
603         * @return                      Arithmetic mean of the values
604         */
605        protected def computeMean( List values ) {
606                def sumOfValues = 0;
607                def sizeOfValues = 0;
608                values.each { value ->
609                        def num = getNumericValue( value );
610                        if( num != null ) {
611                                sumOfValues += num;
612                                sizeOfValues++
613                        } 
614                }
615
616                if( sizeOfValues > 0 )
617                        return sumOfValues / sizeOfValues;
618                else
619                        return 0;
620        }
621
622        /**
623        * Computes the standard error of mean of the given values. 
624        * Values that can not be parsed to a number are ignored. 
625        * If no values are given, the standard deviation of 0 is returned.
626        * @param values         List of values to compute the standard deviation for
627        * @param mean           Mean of the list (if already computed). If not given, the mean
628        *                                       will be computed using the computeMean method
629        * @return                       Standard error of the mean of the values or 0 if no values can be used.
630        */
631   protected def computeSEM( List values, def mean = null ) {
632           if( mean == null )
633                        mean = computeMean( values )
634           
635           def sumOfDifferences = 0;
636           def sizeOfValues = 0;
637           values.each { value ->
638                   def num = getNumericValue( value );
639                   if( num != null ) {
640                           sumOfDifferences += Math.pow( num - mean, 2 );
641                           sizeOfValues++
642                   }
643           }
644
645           if( sizeOfValues > 0 ) {
646                   def std = Math.sqrt( sumOfDifferences / sizeOfValues );
647                   return std / Math.sqrt( sizeOfValues );
648           } else {
649                   return 0;
650           }
651   }
652               
653        /**
654         * Return the numeric value of the given object, or null if no numeric value could be returned
655         * @param       value   Object to return the value for
656         * @return                      Number that represents the given value
657         */
658        protected Number getNumericValue( value ) {
659                // TODO: handle special types of values
660                if( value instanceof Number ) {
661                        return value;
662                } else if( value instanceof RelTime ) {
663                        return value.value;
664                }
665               
666                return null
667        }
668
669        /** 
670         * Returns a field for a given templateentity
671         * @param object        TemplateEntity (or subclass) to retrieve data for
672         * @param fieldName     Name of the field to return data for.
673         * @return                      Value of the field or null if the value could not be retrieved
674         */
675        protected def getFieldValue( TemplateEntity object, String fieldName ) {
676                if( !object || !fieldName )
677                        return null;
678               
679                try {
680                        return object.getFieldValue( fieldName );
681                } catch( Exception e ) {
682                        return null;
683                }
684        }
685
686        /**
687         * Parses a fieldId that has been created earlier by createFieldId
688         * @param fieldId       FieldId to parse
689         * @return                      Map with attributes of the selected field. Keys are 'name', 'id', 'source' and 'type'
690         * @see createFieldId
691         */
692        protected Map parseFieldId( String fieldId ) {
693                def attrs = [:]
694               
695                def parts = fieldId.split(",")
696               
697                attrs = [
698                        "id": parts[ 0 ],
699                        "name": parts[ 1 ],
700                        "source": parts[ 2 ],
701                        "type": parts[ 3 ]
702                ]
703        }
704       
705        /**
706         * Create a fieldId based on the given attributes
707         * @param attrs         Map of attributes for this field. Keys may be 'name', 'id', 'source' and 'type'
708         * @return                      Unique field ID for these parameters
709         * @see parseFieldId
710         */
711        protected String createFieldId( Map attrs ) {
712                // TODO: What if one of the attributes contains a comma?
713                def name = attrs.name;
714                def id = attrs.id ?: name;
715                def source = attrs.source;
716                def type = attrs.type ?: ""
717               
718                return id + "," + name + "," + source + "," + type;
719        }
720
721    protected void returnError(code, msg){
722        response.sendError(code , msg)
723    }
724
725}
Note: See TracBrowser for help on using the repository browser.