source: trunk/grails-app/domain/dbnp/studycapturing/TemplateEntity.groovy @ 496

Last change on this file since 496 was 496, checked in by duh, 12 years ago
  • set svn keyword expansion
  • Property svn:keywords set to Date Rev Author
File size: 19.6 KB
Line 
1package dbnp.studycapturing
2
3import dbnp.data.Term
4import org.springframework.validation.FieldError
5
6/**
7 * TemplateEntity Domain Class
8 *
9 * Revision information:
10 * $Rev: 496 $
11 * $Author: duh $
12 * $Date: 2010-05-28 12:25:13 +0000 (vr, 28 mei 2010) $
13 */
14abstract class TemplateEntity implements Serializable {
15        Template template
16        Map templateStringFields = [:]
17        Map templateTextFields = [:]
18        Map templateStringListFields = [:]
19        Map templateIntegerFields = [:]
20        Map templateFloatFields = [:]
21        Map templateDoubleFields = [:]
22        Map templateDateFields = [:]
23        Map templateRelTimeFields = [:] // Contains relative times in seconds
24        Map templateTermFields = [:]
25
26        static hasMany = [
27                templateStringFields: String,
28                templateTextFields: String,
29                templateStringListFields: TemplateFieldListItem,
30                templateIntegerFields: int,
31                templateFloatFields: float,
32                templateDoubleFields: double,
33                templateDateFields: Date,
34                templateTermFields: Term,
35                templateRelTimeFields: long,
36                systemFields: TemplateField
37        ]
38
39        static mapping = {
40                tablePerHierarchy false
41
42                templateTextFields type: 'text'
43        }       
44
45        /**
46         * Constraints
47         *
48         * All template fields have their own custom validator. Note that there
49         * currently is a lot of code repetition. Ideally we don't want this, but
50         * unfortunately due to scope issues we cannot re-use the code. So make
51         * sure to replicate any changes to all pieces of logic! Only commented
52         * the first occurrence of the logic, please refer to the templateStringFields
53         * validator if you require information about the validation logic...
54         */
55        static constraints = {
56                template(nullable: true, blank: true)
57                templateStringFields(validator: { fields, obj, errors ->
58                        // note that we only use 'fields' and 'errors', 'obj' is
59                        // merely here because it's the way the closure is called
60                        // by the validator...
61
62                        // define a boolean
63                        def error = false
64
65                        // iterate through fields
66                        fields.each { key, value ->
67                                // check if the value is of proper type
68                                if ( value && value.class != String ) {
69                                        // it's of some other type
70                                        try {
71                                                // try to cast it to the proper type
72                                                fields[key] = (value as String)
73                                        } catch (Exception e) {
74                                                // could not typecast properly, value is of improper type
75                                                // add error message
76                                                error = true
77                                                errors.rejectValue(
78                                                        'templateStringFields',
79                                                        'templateEntity.typeMismatch.string',
80                                                        [key, value.class] as Object[],
81                                                        'Property {0} must be of type String and is currently of type {1}'
82                                                )
83                                        }
84                                }
85                        }
86
87                        // got an error, or not?
88                        return (!error)
89                })
90                templateTextFields(validator: { fields, obj, errors ->
91                        def error = false
92                        fields.each { key, value ->
93                                if ( value && value.class != String ) {
94                                        try {
95                                                fields[key] = (value as String)
96                                        } catch (Exception e) {
97                                                error = true
98                                                errors.rejectValue(
99                                                        'templateTextFields',
100                                                        'templateEntity.typeMismatch.string',
101                                                        [key, value.class] as Object[],
102                                                        'Property {0} must be of type String and is currently of type {1}'
103                                                )
104                                        }
105                                }
106                        }
107                        return (!error)
108                })
109                templateStringListFields(validator: { fields, obj, errors ->
110                        def error = false
111                        fields.each { key, value ->
112                                if ( value && value.class != TemplateFieldListItem ) {
113                                        try {
114                                                fields[key] = (value as TemplateFieldListItem)
115                                        } catch (Exception e) {
116                                                error = true
117                                                errors.rejectValue(
118                                                        'templateIntegerFields',
119                                                        'templateEntity.typeMismatch.templateFieldListItem',
120                                                        [key, value.class] as Object[],
121                                                        'Property {0} must be of type TemplateFieldListItem and is currently of type {1}'
122                                                )
123                                        }
124                                }
125                        }
126                        return (!error)
127                })
128                templateIntegerFields(validator: { fields, obj, errors ->
129                        def error = false
130                        fields.each { key, value ->
131                                if (value && value.class != Integer ) {
132                                        try {
133                                                fields[key] = (value as Integer)
134                                        } catch (Exception e) {
135                                                error = true
136                                                errors.rejectValue(
137                                                        'templateIntegerFields',
138                                                        'templateEntity.typeMismatch.integer',
139                                                        [key, value.class] as Object[],
140                                                        'Property {0} must be of type Integer and is currently of type {1}'
141                                                )
142                                        }
143                                }
144                        }
145                        return (!error)
146                })
147                templateFloatFields(validator: { fields, obj, errors ->
148                        def error = false
149                        fields.each { key, value ->
150                                if ( value && value.class != Float ) {
151                                        try {
152                                                fields[key] = (value as Float)
153                                        } catch (Exception e) {
154                                                error = true
155                                                errors.rejectValue(
156                                                        'templateFloatFields',
157                                                        'templateEntity.typeMismatch.float',
158                                                        [key, value.class] as Object[],
159                                                        'Property {0} must be of type Float and is currently of type {1}'
160                                                )
161                                        }
162                                }
163                        }
164                        return (!error)
165                })
166                templateDoubleFields(validator: { fields, obj, errors ->
167                        def error = false
168                        fields.each { key, value ->
169                                if ( value && value.class != Double ) {
170                                        try {
171                                                fields[key] = (value as Double)
172                                        } catch (Exception e) {
173                                                error = true
174                                                errors.rejectValue(
175                                                        'templateDoubleFields',
176                                                        'templateEntity.typeMismatch.double',
177                                                        [key, value.class] as Object[],
178                                                        'Property {0} must be of type Double and is currently of type {1}'
179                                                )
180                                        }
181                                }
182                        }
183                        return (!error)
184                })
185                templateDateFields(validator: { fields, obj, errors ->
186                        def error = false
187                        fields.each { key, value ->
188                                if ( value && value.class != Date ) {
189                                        try {
190                                                fields[key] = (value as Date)
191                                        } catch (Exception e) {
192                                                error = true
193                                                errors.rejectValue(
194                                                        'templateDateFields',
195                                                        'templateEntity.typeMismatch.date',
196                                                        [key, value.class] as Object[],
197                                                        'Property {0} must be of type Date and is currently of type {1}'
198                                                )
199                                        }
200                                }
201                        }
202                        return (!error)
203                })
204                templateRelTimeFields(validator: { fields, obj, errors ->
205                        def error = false
206                        fields.each { key, value ->
207                                if ( value && value.class != long ) {
208                                        try {
209                                                fields[key] = (value as long)
210                                        } catch (Exception e) {
211                                                error = true
212                                                errors.rejectValue(
213                                                        'templateRelTimeFields',
214                                                        'templateEntity.typeMismatch.reltime',
215                                                        [key, value.class] as Object[],
216                                                        'Property {0} must be of type long and is currently of type {1}'
217                                                )
218                                        }
219                                }
220                        }
221                        return (!error)
222                })
223                templateTermFields(validator: { fields, obj, errors ->
224                        def error = false
225                        fields.each { key, value ->
226                                if ( value && value.class != Term ) {
227                                        try {
228                                                fields[key] = (value as Term)
229                                        } catch (Exception e) {
230                                                error = true
231                                                errors.rejectValue(
232                                                        'templateTermFields',
233                                                        'templateEntity.typeMismatch.term',
234                                                        [key, value.class] as Object[],
235                                                        'Property {0} must be of type Term and is currently of type {1}'
236                                                )
237                                        }
238                                }
239                        }
240                        return (!error)
241                })
242        }
243
244        /**
245         * Get the proper templateFields Map for a specific field type
246         * @param TemplateFieldType
247         * @return pointer
248         * @visibility private
249         * @throws NoSuchFieldException
250         */
251        public Map getStore(TemplateFieldType fieldType) {
252                switch(fieldType) {
253                        case TemplateFieldType.STRING:
254                                return templateStringFields
255                        case TemplateFieldType.TEXT:
256                                return templateTextFields
257                        case TemplateFieldType.STRINGLIST:
258                                return templateStringListFields
259                        case TemplateFieldType.INTEGER:
260                                return templateIntegerFields
261                        case TemplateFieldType.DATE:
262                                return templateDateFields
263                        case TemplateFieldType.RELTIME:
264                                return templateRelTimeFields
265                        case TemplateFieldType.FLOAT:
266                                return templateFloatFields
267                        case TemplateFieldType.DOUBLE:
268                                return templateDoubleFields
269                        case TemplateFieldType.ONTOLOGYTERM:
270                                return templateTermFields
271                        default:
272                                throw new NoSuchFieldException("Field type ${fieldType} not recognized")
273                }
274        }
275
276        /**
277         * Find a field domain or template field by its name and return its description
278         * @param fieldsCollection the set of fields to search in, usually something like this.giveFields()
279         * @param fieldName The name of the domain or template field
280         * @return the TemplateField description of the field
281         * @throws NoSuchFieldException If the field is not found or the field type is not supported
282         */
283        private static TemplateField getField(List<TemplateField> fieldsCollection, String fieldName) {
284                // escape the fieldName for easy matching
285                // (such escaped names are commonly used
286                // in the HTTP forms of this application)
287                String escapedLowerCaseFieldName = fieldName.toLowerCase().replaceAll("([^a-z0-9])","_")
288
289                // Find the target template field, if not found, throw an error
290                TemplateField field = fieldsCollection.find { it.name.toLowerCase().replaceAll("([^a-z0-9])", "_") == escapedLowerCaseFieldName }
291
292                if (field) {
293                        return field
294                }
295                else {
296                        throw new NoSuchFieldException("Field ${fieldName} not recognized")
297                }
298        }
299
300        /**
301         * Find a domain or template field by its name and return its value for this entity
302         * @param fieldName The name of the domain or template field
303         * @return the value of the field (class depends on the field type)
304         * @throws NoSuchFieldException If the field is not found or the field type is not supported
305         */
306        def getFieldValue(String fieldName) {
307
308                if (isDomainField(fieldName)) {
309                    return this[fieldName]
310                }
311                else {
312                    TemplateField field = getField(this.giveTemplateFields(),fieldName)
313                    return getStore(field.type)[fieldName]
314                }
315
316        }
317
318        /**
319         * Check whether a given template field exists or not
320         * @param fieldName The name of the template field
321         * @return true if the given field exists and false otherwise
322         */
323        boolean fieldExists(String fieldName) {
324                // getField should throw a NoSuchFieldException if the field does not exist
325                try {
326                        TemplateField field = getField(this.giveFields(),fieldName)
327                        // return true if exception is not thrown (but double check if field really is not null)
328                        if (field) {
329                                return true
330                        }
331                        else {
332                                return false
333                        }
334                }
335                // if exception is thrown, return false
336                catch(NoSuchFieldException e) {
337                        return false
338                }
339        }
340
341        /**
342         * Set a template/entity field value
343         * @param fieldName The name of the template or entity field
344         * @param value The value to be set, this should align with the (template) field type, but there are some convenience setters
345         */
346        def setFieldValue(String fieldName, value) {
347                // get the template field
348                TemplateField field = getField(this.giveFields(),fieldName)
349
350                // Convenience setter for template string list fields: find TemplateFieldListItem by name
351                if (field.type == TemplateFieldType.STRINGLIST && value && value.class == String) {
352                        // Kees insensitive pattern matching ;)
353                        def escapedLowerCaseValue = value.toLowerCase().replaceAll("([^a-z0-9])", "_")
354                        value = field.listEntries.find {
355                                it.name.toLowerCase().replaceAll("([^a-z0-9])", "_") == escapedLowerCaseValue
356                        }
357                }
358
359                // Magic setter for dates: handle string values for date fields
360                if (field.type == TemplateFieldType.DATE && value && value.class == String) {
361                        // a string was given, attempt to transform it into a date instance
362                        // and -for now- assume the dd/mm/yyyy format
363                        def dateMatch = value =~ /^([0-9]{1,})([^0-9]{1,})([0-9]{1,})([^0-9]{1,})([0-9]{1,})((([^0-9]{1,})([0-9]{1,2}):([0-9]{1,2})){0,})/
364                        if (dateMatch.matches()) {
365                                // create limited 'autosensing' datetime parser
366                                // assume dd mm yyyy  or dd mm yy
367                                def parser = 'd' + dateMatch[0][2] + 'M' + dateMatch[0][4] + (((dateMatch[0][5] as int) > 999) ? 'yyyy' : 'yy')
368
369                                // add time as well?
370                                if (dateMatch[0][7] != null) {
371                                        parser += dateMatch[0][6] + 'HH:mm'
372                                }
373
374                                value = new Date().parse(parser, value)
375                        }
376                }
377
378                // Magic setter for relative times: handle string values for relTime fields
379                //
380                // The relative time may be set as a string, using the following format
381                //
382                //    #w #d #h #m #s
383                //
384                // Where w = weeks, d = days, h = hours, m = minutes, s = seconds
385                //
386                // The spaces between the values are optional. Every timespan
387                // (w, d, h, m, s) must appear at most once. You can also omit
388                // timespans if needed or use a different order.
389                // Other characters are disregarded, allthough results may not
390                // always be as expected.
391                // 
392                // If an incorrect format is used, which can't be parsed
393                // an IllegalArgumentException is thrown.
394                //
395                // An empty span is treated as zero seconds.
396                //
397                // Examples:
398                // ---------
399                //    5d 3h 20m     // 5 days, 3 hours and 20 minutes
400                //    6h 2d         // 2 days, 6 hours
401                //    10m 200s      // 13 minutes, 20 seconds (200s == 3m + 20s)
402                //    5w4h15m       // 5 weeks, 4 hours, 15 minutes
403                //
404                //    16x14w10d     // Incorrect. 16x is disregarded, so the
405                //                  // result is 15 weeks, 3 days
406                //    13days        // Incorrect: days should be d, but this is
407                //                  // parsed as 13d, 0 seconds
408                //
409                if (field.type == TemplateFieldType.RELTIME && value.class == String) {
410                        // A string was given, attempt to transform it into a timespan
411
412                        // An empty string should be parsed as 0
413                        if( value.trim() == "" ) {
414                            value = 0;
415                        } else {
416                            // Find all parts that contain numbers with
417                            // a character w, d, h, m or s after it
418                            def periodMatch = value =~ /([0-9]+)([wdhms])/
419                            if (periodMatch.size() > 0 ) {
420                                    def seconds = 0L;
421
422                                    // Now check if every part contains data for
423                                    // the time interval
424                                    periodMatch.each {
425                                        def partValue
426
427                                        println it
428
429                                        if( it[1].isLong() ) {
430                                            partValue = Long.parseLong( it[1] );
431                                        } else {
432                                            partValue = 0;
433                                        }
434
435                                        switch( it[ 2 ] ) {
436                                            case 'w':
437                                                seconds += 7L * 24 * 60 * 60 * partValue;
438                                                break;
439                                            case 'd':
440                                                seconds += 24L * 60 * 60 * partValue;
441                                                break;
442                                            case 'h':
443                                                seconds += 60L * 60 * partValue;
444                                                break;
445                                            case 'm':
446                                                seconds += 60L * partValue;
447                                                break;
448                                            case 's':
449                                                seconds += partValue;
450                                                break;
451                                            default:
452                                                adf.error.warn( 'Parsing relative time: ' + it[0] + it[1] + ' is not understood and disregarded' );
453                                                break;
454                                        }
455                                    }
456
457                                    // Continue with the computed value
458                                    value = seconds;
459                            } else {
460                                throw new IllegalArgumentException( "String " + value + " cannot be parsed as a relative time. Use format #w #d #h #m #s." );
461                            }
462                        }
463                }
464
465                // Magic setter for ontology terms: handle string values
466                if (field.type == TemplateFieldType.ONTOLOGYTERM && value && value.class == String) {
467                        // iterate through ontologies and find term
468                        field.ontologies.each() { ontology ->
469                                def term = ontology.giveTermByName(value)
470
471                                // found a term?
472                                if (term) {
473                                        value = term
474                                }
475                        }
476                }
477
478                // Set the field value
479                if (isDomainField(field)) {
480                        // got a value?
481                        if (value) {
482                                println ".setting [" + ((super) ? super.class : '??') + "] domain field: [" + fieldName + "] ([" + value.toString() + "] of type [" + value.class + "])"
483
484                                // set value
485                                this[field.name] = value
486                        } else {
487                                println ".unsetting [" + ((super) ? super.class : '??') + "] domain field: [" + fieldName + "]"
488
489                                // remove value
490                                this[field.name] = null
491                        }
492                } else {
493                        // Caution: this assumes that all template...Field Maps are already initialized (as is done now above as [:])
494                        // If that is ever changed, the results are pretty much unpredictable (random Java object pointers?)!
495                        def store = getStore(field.type)
496
497                        // If some value is entered (or 0), then save the value
498                        // otherwise, it should not be present in the store, so
499                        // it is unset if it is.
500                        if ( value || value == 0 ) {
501                            println ".setting [" + ((super) ? super.class : '??') + "] template field: [" + fieldName + "] ([" + value.toString() + "] of type [" + value.class + "])"
502
503                            // set value
504                            store[fieldName] = value
505                        } else if ( store[fieldName] ) {
506                            println ".unsetting [" + ((super) ? super.class : '??') + "] template field: [" + fieldName + "]"
507
508                            // remove the item from the Map (if present)
509                            store.remove(fieldName)
510                        }
511                }
512
513                return this
514        }
515
516        /**
517         * Check if a given field is a domain field
518         * @param TemplateField         field instance
519         * @return boolean
520         */
521        boolean isDomainField(TemplateField field) {
522            return isDomainField( field.name )
523        }
524
525        /**
526         * Check if a given field is a domain field
527         * @param String        field name
528         * @return boolean
529         */
530        boolean isDomainField(String fieldName) {
531            return this.giveDomainFields()*.name.contains(fieldName)
532        }
533       
534        /**
535         * Return all fields defined in the underlying template and the built-in
536         * domain fields of this entity
537         */
538        def List<TemplateField> giveFields() {
539                return this.giveDomainFields() + this.giveTemplateFields();
540        }
541
542        /**
543         * Return all templated fields defined in the underlying template of this entity
544         */
545        def List<TemplateField> giveTemplateFields() {
546                return (this.template) ? this.template.fields : []
547        }
548
549        /**
550         * Return all relevant 'built-in' domain fields of the super class
551         * @return List with DomainTemplateFields
552     * @see TemplateField
553         */
554        abstract List<TemplateField> giveDomainFields()
555
556        /**
557         * Convenience method. Returns all unique templates used within a collection of TemplateEntities.
558         */
559        static Set<Template> giveTemplates(Set<TemplateEntity> entityCollection) {
560                return entityCollection*.template.unique();
561        }
562
563        /**
564         * Convenience method. Returns the template used within a collection of TemplateEntities.
565         * @throws NoSuchFieldException when 0 or multiple templates are used in the collection
566         * @return The template used by all members of a collection
567         */
568        static Template giveSingleTemplate(Set<TemplateEntity> entityCollection) {
569                def templates = giveTemplates(entityCollection);
570                if (templates.size() == 0) {
571                        throw new NoSuchFieldException("No templates found in collection!")
572                } else if (templates.size() == 1) {
573                        return templates[0];
574                } else {
575                        throw new NoSuchFieldException("Multiple templates found in collection!")
576                }
577        }
578}
Note: See TracBrowser for help on using the repository browser.