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

Last change on this file since 754 was 754, checked in by keesvb, 12 years ago

big refactoring / change of the data model: implemented belongsTo everywhere where it should be, added comments to the domain classes, changed BootStrap? accordingly

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