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

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