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

Last change on this file since 538 was 538, checked in by roberth, 13 years ago

Improved template editor. Moving fields and basic editing works already.

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