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

Last change on this file since 559 was 559, checked in by roberth, 12 years ago

Added boolean template field type

  • Property svn:keywords set to Date Rev Author
File size: 19.9 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: 559 $
11 * $Author: roberth $
12 * $Date: 2010-06-11 14:22:26 +0000 (vr, 11 jun 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                if( field.type == TemplateFieldType.BOOLEAN ) {
421                        println( 'Boolean: ' + fieldName + " - " + value );
422                }
423
424                // Convenience setter for template string list fields: find TemplateFieldListItem by name
425                if (field.type == TemplateFieldType.STRINGLIST && value && value.class == String) {
426                        // Kees insensitive pattern matching ;)
427                        def escapedLowerCaseValue = value.toLowerCase().replaceAll("([^a-z0-9])", "_")
428                        value = field.listEntries.find {
429                                it.name.toLowerCase().replaceAll("([^a-z0-9])", "_") == escapedLowerCaseValue
430                        }
431                }
432
433                // Magic setter for dates: handle string values for date fields
434                if (field.type == TemplateFieldType.DATE && value && value.class == String) {
435                        // a string was given, attempt to transform it into a date instance
436                        // and -for now- assume the dd/mm/yyyy format
437                        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,})/
438                        if (dateMatch.matches()) {
439                                // create limited 'autosensing' datetime parser
440                                // assume dd mm yyyy  or dd mm yy
441                                def parser = 'd' + dateMatch[0][2] + 'M' + dateMatch[0][4] + (((dateMatch[0][5] as int) > 999) ? 'yyyy' : 'yy')
442
443                                // add time as well?
444                                if (dateMatch[0][7] != null) {
445                                        parser += dateMatch[0][6] + 'HH:mm'
446                                }
447
448                                value = new Date().parse(parser, value)
449                        }
450                }
451
452                // Magic setter for relative times: handle string values for relTime fields
453                //
454                if (field.type == TemplateFieldType.RELTIME && value != null && value.class == String) {
455                        // A string was given, attempt to transform it into a timespan
456                        // If it cannot be parsed, set the lowest possible value of Long.
457                        // The validator method will raise an error
458                        //
459                        // N.B. If you try to set Long.MIN_VALUE itself, an error will occur
460                        // However, this will never occur: this value represents 3 bilion centuries
461                        try {
462                                value = RelTime.parseRelTime(value).getValue();
463                        } catch (IllegalArgumentException e) {
464                                value = Long.MIN_VALUE;
465                        }
466                }
467
468                // Sometimes the fileService is not created yet
469                if (!fileService) {
470                        fileService = new FileService();
471                }
472
473                // Magic setter for files: handle values for file fields
474                //
475                // If NULL is given, the field value is emptied and the old file is removed
476                // If an empty string is given, the field value is kept as was
477                // If a file is given, it is moved to the right directory. Old files are deleted. If
478                //   the file does not exist, the field is kept
479                // If a string is given, it is supposed to be a file in the upload directory. If
480                //   it is different from the old one, the old one is deleted. If the file does not
481                //   exist, the old one is kept.
482                if (field.type == TemplateFieldType.FILE) {
483                        def currentFile = getFieldValue(field.name);
484
485                        if (value == null) {
486                                // If NULL is given, the field value is emptied and the old file is removed
487                                value = "";
488                                if (currentFile) {
489                                        fileService.delete(currentFile)
490                                }
491                        } else if (value.class == File) {
492                                // a file was given. Attempt to move it to the upload directory, and
493                                // afterwards, store the filename. If the file doesn't exist
494                                // or can't be moved, "" is returned
495                                value = fileService.moveFileToUploadDir(value);
496
497                                if (value) {
498                                        if (currentFile) {
499                                                fileService.delete(currentFile)
500                                        }
501                                } else {
502                                        value = currentFile;
503                                }
504                        } else if (value == "") {
505                                value = currentFile;
506                        } else {
507                                if (value != currentFile) {
508                                        if (fileService.fileExists(value)) {
509                                                // When a FILE field is filled, and a new file is set
510                                                // the existing file should be deleted
511                                                if (currentFile) {
512                                                        fileService.delete(currentFile)
513                                                }
514                                        } else {
515                                                // If the file does not exist, the field is kept
516                                                value = currentFile;
517                                        }
518                                }
519                        }
520                }
521
522                // Magic setter for ontology terms: handle string values
523                if (field.type == TemplateFieldType.ONTOLOGYTERM && value && value.class == String) {
524                        // iterate through ontologies and find term
525                        field.ontologies.each() { ontology ->
526                                def term = ontology.giveTermByName(value)
527
528                                // found a term?
529                                if (term) {
530                                        value = term
531                                }
532                        }
533                }
534
535                // Set the field value
536                if (isDomainField(field)) {
537                        // got a value?
538                        if (value) {
539                                //debug message: println ".setting [" + ((super) ? super.class : '??') + "] domain field: [" + fieldName + "] ([" + value.toString() + "] of type [" + value.class + "])"
540
541                                // set value
542                                this[field.name] = value
543                        } else {
544                                //debug message: println ".unsetting [" + ((super) ? super.class : '??') + "] domain field: [" + fieldName + "]"
545
546                                // remove value. For numbers, this is done by setting
547                                // the value to 0, otherwise, setting it to NULL
548                                switch (field.type.toString()) {
549                                        case ['INTEGER', 'FLOAT', 'DOUBLE', 'RELTIME']:
550                                                this[field.name] = 0;
551                                                break;
552                                        case [ 'BOOLEAN' ]:
553                                                this[field.name] = false;
554                                                break;
555                                        default:
556                                                this[field.name] = null
557                                }
558                        }
559                } else {
560                        // Caution: this assumes that all template...Field Maps are already initialized (as is done now above as [:])
561                        // If that is ever changed, the results are pretty much unpredictable (random Java object pointers?)!
562                        def store = getStore(field.type)
563
564                        // If some value is entered (or 0), then save the value
565                        // otherwise, it should not be present in the store, so
566                        // it is unset if it is.
567                        if (value || value == 0) {
568                                println ".setting [" + ((super) ? super.class : '??') + "] template field: [" + fieldName + "] ([" + value.toString() + "] of type [" + value.class + "])"
569
570                                // set value
571                                store[fieldName] = value
572                        } else if (store[fieldName]) {
573                                println ".unsetting [" + ((super) ? super.class : '??') + "] template field: [" + fieldName + "]"
574
575                                // remove the item from the Map (if present)
576                                store.remove(fieldName)
577                        }
578                }
579
580                return this
581        }
582
583        /**
584         * Check if a given field is a domain field
585         * @param TemplateField field instance
586         * @return boolean
587         */
588        boolean isDomainField(TemplateField field) {
589                return isDomainField(field.name)
590        }
591
592        /**
593         * Check if a given field is a domain field
594         * @param String field name
595         * @return boolean
596         */
597        boolean isDomainField(String fieldName) {
598                return this.giveDomainFields()*.name.contains(fieldName)
599        }
600
601        /**
602         * Return all fields defined in the underlying template and the built-in
603         * domain fields of this entity
604         */
605        def List<TemplateField> giveFields() {
606                return this.giveDomainFields() + this.giveTemplateFields();
607        }
608
609        /**
610         * Return all templated fields defined in the underlying template of this entity
611         */
612        def List<TemplateField> giveTemplateFields() {
613                return (this.template) ? this.template.fields : []
614        }
615
616        /**
617         * Return all relevant 'built-in' domain fields of the super class. Should be implemented by a static method
618         * @return List with DomainTemplateFields
619         * @see TemplateField
620         */
621        abstract List<TemplateField> giveDomainFields()
622
623        /**
624         * Convenience method. Returns all unique templates used within a collection of TemplateEntities.
625         *
626         * If the collection is empty, an empty set is returned. If none of the entities contains
627         * a template, also an empty set is returned.
628         */
629        static Set<Template> giveTemplates(Set<TemplateEntity> entityCollection) {
630                def set = entityCollection*.template.unique();
631
632                // If one or more entities does not have a template, the resulting
633                // set contains null. That is not what is meant.
634                return set.findAll { it != null };
635        }
636
637        /**
638         * Convenience method. Returns the template used within a collection of TemplateEntities.
639         * @throws NoSuchFieldException when 0 or multiple templates are used in the collection
640         * @return The template used by all members of a collection
641         */
642        static Template giveSingleTemplate(Set<TemplateEntity> entityCollection) {
643                def templates = giveTemplates(entityCollection);
644                if (templates.size() == 0) {
645                        throw new NoSuchFieldException("No templates found in collection!")
646                } else if (templates.size() == 1) {
647                        return templates[0];
648                } else {
649                        throw new NoSuchFieldException("Multiple templates found in collection!")
650                }
651        }
652}
Note: See TracBrowser for help on using the repository browser.