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

Last change on this file since 536 was 536, checked in by roberth, 13 years ago

Samples are now shown in the study overview page. Also some tests are added and the bootstrap is updated to show some sample fields with a template

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