root/trunk/grails-app/domain/dbnp/studycapturing/TemplateEntity.groovy @ 661

Revision 661, 20.6 KB (checked in by tabma, 4 years ago)

- fixed property assignment by using lookup from new entity object method getFieldType

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