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

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