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

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

restructured BootStrap?, it should now run faster because there are no BioPortal? connections, and it adds sample studies in development mode (outcomment the addExampleStudies call in the BootStrap? if you don't want that). Renamed TemplateEntity?.getFieldType to giveFieldType and added tests to SampleTests? for the method.

  • Property svn:keywords set to Date Rev Author
File size: 20.6 KB
Line 
1package dbnp.studycapturing
2
3import dbnp.data.Term
4import org.springframework.validation.FieldError
5
6/**
7 * TemplateEntity Domain Class
8 *
9 * Revision information:
10 * $Rev: 662 $
11 * $Author: keesvb $
12 * $Date: 2010-07-19 14:28:10 +0000 (ma, 19 jul 2010) $
13 */
14abstract class TemplateEntity implements Serializable {
15        Template template
16        Map templateStringFields = [:]
17        Map templateTextFields = [:]
18        Map templateStringListFields = [:]
19        Map templateIntegerFields = [:]
20        Map templateFloatFields = [:]
21        Map templateDoubleFields = [:]
22        Map templateDateFields = [:]
23        Map 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         * Look up the type of a certain template field
626         * @param String fieldName The name of the template field
627         * @return String The type (static member of TemplateFieldType) of the field, or null of the field does not exist
628         */
629        TemplateFieldType giveFieldType(String fieldName) {
630                def field = giveFields().find {
631                        it.name == fieldName
632                }
633                field?.type
634        }
635
636        /**
637         * Return all relevant 'built-in' domain fields of the super class. Should be implemented by a static method
638         * @return List with DomainTemplateFields
639         * @see TemplateField
640         */
641        abstract List<TemplateField> giveDomainFields()
642
643        /**
644         * Convenience method. Returns all unique templates used within a collection of TemplateEntities.
645         *
646         * If the collection is empty, an empty set is returned. If none of the entities contains
647         * a template, also an empty set is returned.
648         */
649        static Collection<Template> giveTemplates(Collection<TemplateEntity> entityCollection) {
650                def set = entityCollection*.template.unique();
651
652                // If one or more entities does not have a template, the resulting
653                // set contains null. That is not what is meant.
654                return set.findAll { it != null };
655        }
656
657        /**
658         * Convenience method. Returns the template used within a collection of TemplateEntities.
659         * @throws NoSuchFieldException when 0 or multiple templates are used in the collection
660         * @return The template used by all members of a collection
661         */
662        static Template giveSingleTemplate(Collection<TemplateEntity> entityCollection) {
663                def templates = giveTemplates(entityCollection);
664                if (templates.size() == 0) {
665                        throw new NoSuchFieldException("No templates found in collection!")
666                } else if (templates.size() == 1) {
667                        return templates[0];
668                } else {
669                        throw new NoSuchFieldException("Multiple templates found in collection!")
670                }
671        }
672
673}
Note: See TracBrowser for help on using the repository browser.