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

Revision 1264, 31.8 KB (checked in by work@…, 3 years ago)

resolves issue #233, validation of an object which inherits from TemplateEntity? does not result in an error when required templateFields are missing.

  • Property svn:keywords set to Author Date Rev
Line 
1package dbnp.studycapturing
2
3import dbnp.data.Term
4import org.springframework.validation.FieldError
5
6/**
7 * The TemplateEntity domain Class is a superclass for all template-enabled study capture entities, including
8 * Study, Subject, Sample and Event. This class provides functionality for storing the different TemplateField
9 * values and returning the combined list of 'domain fields' and 'template fields' of a TemplateEntity.
10 * For an explanation of those terms, see the Template class.
11 *
12 * @see dbnp.studycapturing.Template
13 *
14 * Revision information:
15 * $Rev$
16 * $Author$
17 * $Date$
18 */
19abstract class TemplateEntity extends Identity {
20        // The actual template of this TemplateEntity instance
21        Template template
22
23        // Maps for storing the different template field values
24        Map templateStringFields        = [:]
25        Map templateTextFields          = [:]
26        Map templateStringListFields= [:]
27        Map templateDoubleFields        = [:]
28        Map templateDateFields          = [:]
29        Map templateBooleanFields       = [:]
30        Map templateTemplateFields      = [:]
31        Map templateModuleFields        = [:]
32        Map templateLongFields          = [:]
33
34        // N.B. If you try to set Long.MIN_VALUE for a reltime field, an error will occur
35        // However, this will never occur in practice: this value represents 3 bilion centuries
36        Map templateRelTimeFields       = [:] // Contains relative times in seconds
37        Map templateFileFields          = [:] // Contains filenames
38        Map templateTermFields          = [:]
39
40        // define relationships
41        static hasMany = [
42                templateStringFields    : String,
43                templateTextFields              : String,
44                templateStringListFields: TemplateFieldListItem,
45                templateDoubleFields    : double,
46                templateDateFields              : Date,
47                templateTermFields              : Term,
48                templateRelTimeFields   : long,
49                templateFileFields              : String,
50                templateBooleanFields   : boolean,
51                templateTemplateFields  : Template,
52                templateModuleFields    : AssayModule,
53                templateLongFields              : long,
54                systemFields                    : TemplateField
55        ]
56
57        // remember required fields when
58        // so we can validate is the required
59        // template fields are set
60        boolean requiredFieldsSet       = false
61        Set requiredFields                      = []
62
63        /**
64         * Get the required fields for the defined template, currently
65         * this method is called in custom validators below but it's
66         * best to call it in a template setter method. But as that
67         * involves a lot of refactoring this implementation will do
68         * fine for now.
69         *
70         * Another possible issue might be that if the template is
71         * updated after the required fields are cached in the object.
72         *
73         * @return Set  requiredFields
74         */
75        final Set getRequiredFields() {
76                if (template) {
77                        // template is set, check if required fields
78                        // have already been fetched
79                        if (!requiredFieldsSet) {
80                                // no, fetch required fields
81                                requiredFields          = template.getRequiredFields()
82                                requiredFieldsSet       = true
83                        }
84
85                        // return the required fields
86                        return requiredFields
87                } else {
88                        // template is not yet set, return
89                        // an empty set
90                        return []
91                }
92        }
93
94        // overload transients from Identity
95        // and append requiredFields vars
96        static transients                       = [ "identifier", "iterator", "maximumIdentity", "requiredFieldsSet", "requiredFields" ]
97
98        // define the mapping
99        static mapping = {
100                // Specify that each TemplateEntity-subclassing entity should have its own tables to store TemplateField values.
101                // This results in a lot of tables, but performance is presumably better because in most queries, only values of
102                // one specific entity will be retrieved. Also, because of the generic nature of these tables, they can end up
103                // containing a lot of records (there is a record for each entity instance for each property, instead of a record
104                // for each instance as is the case with 'normal' straightforward database tables. Therefore, it's better to split
105                // out the data to many tables.
106                tablePerHierarchy false
107
108                // Make sure that the text fields are really stored as TEXT, so that those Strings can have an arbitrary length.
109                templateTextFields type: 'text'
110        }
111
112        // Inject the service for storing files (for TemplateFields of TemplateFieldType FILE).
113        def fileService
114
115        /**
116         * Constraints
117         *
118         * All template fields have their own custom validator. Note that there
119         * currently is a lot of code repetition. Ideally we don't want this, but
120         * unfortunately due to scope issues we cannot re-use the code. So make
121         * sure to replicate any changes to all pieces of logic! Only commented
122         * the first occurrence of the logic, please refer to the templateStringFields
123         * validator if you require information about the validation logic...
124         */
125        static constraints = {
126                template(nullable: true, blank: true)
127                templateStringFields(validator: { fields, obj, errors ->
128                        // note that we only use 'fields' and 'errors', 'obj' is
129                        // merely here because it's the way the closure is called
130                        // by the validator...
131
132                        // define a boolean
133                        def error = false
134
135                        // iterate through fields
136                        fields.each { key, value ->
137                                // check if the value is of proper type
138                                if (value) {
139                                        def strValue = "";
140                                        if( value.class != String) {
141                                                // it's of some other type
142                                                try {
143                                                        // try to cast it to the proper type
144                                                        strValue = (value as String)
145                                                        fields[key] = strValue
146                                                } catch (Exception e) {
147                                                        // could not typecast properly, value is of improper type
148                                                        // add error message
149                                                        error = true
150                                                        errors.rejectValue(
151                                                                'templateStringFields',
152                                                                'templateEntity.typeMismatch.string',
153                                                                [key, value.class] as Object[],
154                                                                'Property {0} must be of type String and is currently of type {1}'
155                                                        )
156                                                }
157                                        } else {
158                                                strValue = value;
159                                        }
160
161                                        // Check whether the string doesn't exceed 255 characters
162                                        if( strValue.size() > 255 ) {
163                                                error = true
164                                                errors.rejectValue(
165                                                        'templateStringFields',
166                                                        'templateEntity.tooLong.string',
167                                                        [key] as Object[],
168                                                        'Property {0} may contain at most 255 characters.'
169                                                )
170                                        }
171                                }
172                        }
173
174                        // validating the required template fields. Note that the
175                        // getRequiredFields() are cached in this object, so any
176                        // template changes after caching may not be validated
177                        // properly.
178                        obj.getRequiredFields().findAll { it.type == TemplateFieldType.STRING }.each { field ->
179                                if (! fields.find { key, value -> key == field.name } ) {
180                                        // required field is missing
181                                        error = true
182                                        errors.rejectValue(
183                                                'templateStringFields',
184                                                'templateEntity.required',
185                                                [field.name] as Object[],
186                                                'Property {0} is required but it missing'
187                                        )
188                                }
189                        }
190
191                        // got an error, or not?
192                        return (!error)
193                })
194                templateTextFields(validator: { fields, obj, errors ->
195                        def error = false
196                        fields.each { key, value ->
197                                if (value && value.class != String) {
198                                        try {
199                                                fields[key] = (value as String)
200                                        } catch (Exception e) {
201                                                error = true
202                                                errors.rejectValue(
203                                                        'templateTextFields',
204                                                        'templateEntity.typeMismatch.string',
205                                                        [key, value.class] as Object[],
206                                                        'Property {0} must be of type String and is currently of type {1}'
207                                                )
208                                        }
209                                }
210                        }
211
212                        // validating required fields
213                        obj.getRequiredFields().findAll { it.type == TemplateFieldType.TEXT }.each { field ->
214                                if (! fields.find { key, value -> key == field.name } ) {
215                                        // required field is missing
216                                        error = true
217                                        errors.rejectValue(
218                                                'templateTextFields',
219                                                'templateEntity.required',
220                                                [field.name] as Object[],
221                                                'Property {0} is required but it missing'
222                                        )
223                                }
224                        }
225
226                        return (!error)
227                })
228                templateStringListFields(validator: { fields, obj, errors ->
229                        def error = false
230                        fields.each { key, value ->
231                                if (value && value.class != TemplateFieldListItem) {
232                                        try {
233                                                fields[key] = (value as TemplateFieldListItem)
234                                        } catch (Exception e) {
235                                                error = true
236                                                errors.rejectValue(
237                                                        'templateStringFields',
238                                                        'templateEntity.typeMismatch.templateFieldListItem',
239                                                        [key, value.class] as Object[],
240                                                        'Property {0} must be of type TemplateFieldListItem and is currently of type {1}'
241                                                )
242                                        }
243                                }
244                        }
245
246                        // validating required fields
247                        obj.getRequiredFields().findAll { it.type == TemplateFieldType.STRINGLIST }.each { field ->
248                                if (! fields.find { key, value -> key == field.name } ) {
249                                        // required field is missing
250                                        error = true
251                                        errors.rejectValue(
252                                                'templateStringFields',
253                                                'templateEntity.required',
254                                                [field.name] as Object[],
255                                                'Property {0} is required but it missing'
256                                        )
257                                }
258                        }
259
260                        return (!error)
261                })
262                templateDoubleFields(validator: { fields, obj, errors ->
263                        def error = false
264                        fields.each { key, value ->
265                                // Double field should accept Doubles and Floats
266                                if (value && value.class != Double) {
267                                        if(value.class == Float) {
268                                                fields[key] = value.toDouble();
269                                        } else {
270                                                try {
271                                                        fields[key] = (value as Double)
272                                                } catch (Exception e) {
273                                                        error = true
274                                                        errors.rejectValue(
275                                                                'templateDoubleFields',
276                                                                'templateEntity.typeMismatch.double',
277                                                                [key, value.class] as Object[],
278                                                                'Property {0} must be of type Double and is currently of type {1}'
279                                                        )
280                                                }
281                                        }
282                                }
283                        }
284
285                        // validating required fields
286                        obj.getRequiredFields().findAll { it.type == TemplateFieldType.DOUBLE }.each { field ->
287                                if (! fields.find { key, value -> key == field.name } ) {
288                                        // required field is missing
289                                        error = true
290                                        errors.rejectValue(
291                                                'templateDoubleFields',
292                                                'templateEntity.required',
293                                                [field.name] as Object[],
294                                                'Property {0} is required but it missing'
295                                        )
296                                }
297                        }
298
299                        return (!error)
300                })
301                templateDateFields(validator: { fields, obj, errors ->
302                        def error = false
303                        fields.each { key, value ->
304                                if (value && value.class != Date) {
305                                        try {
306                                                fields[key] = (value as Date)
307                                        } catch (Exception e) {
308                                                error = true
309                                                errors.rejectValue(
310                                                        'templateDateFields',
311                                                        'templateEntity.typeMismatch.date',
312                                                        [key, value.class] as Object[],
313                                                        'Property {0} must be of type Date and is currently of type {1}'
314                                                )
315                                        }
316                                }
317                        }
318
319                        // validating required fields
320                        obj.getRequiredFields().findAll { it.type == TemplateFieldType.DATE }.each { field ->
321                                if (! fields.find { key, value -> key == field.name } ) {
322                                        // required field is missing
323                                        error = true
324                                        errors.rejectValue(
325                                                'templateDateFields',
326                                                'templateEntity.required',
327                                                [field.name] as Object[],
328                                                'Property {0} is required but it missing'
329                                        )
330                                }
331                        }
332
333                        return (!error)
334                })
335                templateRelTimeFields(validator: { fields, obj, errors ->
336                        def error = false
337                        fields.each { key, value ->
338                                if (value && value == Long.MIN_VALUE) {
339                                        error = true
340                                        errors.rejectValue(
341                                                'templateRelTimeFields',
342                                                'templateEntity.typeMismatch.reltime',
343                                                [key, value] as Object[],
344                                                'Value cannot be parsed for property {0}'
345                                        )
346                                } else if (value && value.class != long) {
347                                        try {
348                                                fields[key] = (value as long)
349                                        } catch (Exception e) {
350                                                error = true
351                                                errors.rejectValue(
352                                                        'templateRelTimeFields',
353                                                        'templateEntity.typeMismatch.reltime',
354                                                        [key, value.class] as Object[],
355                                                        'Property {0} must be of type long and is currently of type {1}'
356                                                )
357                                        }
358                                }
359                        }
360
361                        // validating required fields
362                        obj.getRequiredFields().findAll { it.type == TemplateFieldType.RELTIME }.each { field ->
363                                if (! fields.find { key, value -> key == field.name } ) {
364                                        // required field is missing
365                                        error = true
366                                        errors.rejectValue(
367                                                'templateRelTimeFields',
368                                                'templateEntity.required',
369                                                [field.name] as Object[],
370                                                'Property {0} is required but it missing'
371                                        )
372                                }
373                        }
374
375                        return (!error)
376                })
377                templateTermFields(validator: { fields, obj, errors ->
378                        def error = false
379                        fields.each { key, value ->
380                                if (value && value.class != Term) {
381                                        try {
382                                                fields[key] = (value as Term)
383                                        } catch (Exception e) {
384                                                error = true
385                                                errors.rejectValue(
386                                                        'templateTermFields',
387                                                        'templateEntity.typeMismatch.term',
388                                                        [key, value.class] as Object[],
389                                                        'Property {0} must be of type Term and is currently of type {1}'
390                                                )
391                                        }
392                                }
393                        }
394
395                        // validating required fields
396                        obj.getRequiredFields().findAll { it.type == TemplateFieldType.ONTOLOGYTERM }.each { field ->
397                                if (! fields.find { key, value -> key == field.name } ) {
398                                        // required field is missing
399                                        error = true
400                                        errors.rejectValue(
401                                                'templateTermFields',
402                                                'templateEntity.required',
403                                                [field.name] as Object[],
404                                                'Property {0} is required but it missing'
405                                        )
406                                }
407                        }
408
409                        return (!error)
410                })
411                templateFileFields(validator: { fields, obj, errors ->
412                        // note that we only use 'fields' and 'errors', 'obj' is
413                        // merely here because it's the way the closure is called
414                        // by the validator...
415
416                        // define a boolean
417                        def error = false
418
419                        // iterate through fields
420                        fields.each { key, value ->
421                                // check if the value is of proper type
422                                if (value && value.class != String) {
423                                        // it's of some other type
424                                        try {
425                                                // try to cast it to the proper type
426                                                fields[key] = (value as String)
427
428                                                // Find the file on the system
429                                                // if it does not exist, the filename can
430                                                // not be entered
431
432                                        } catch (Exception e) {
433                                                // could not typecast properly, value is of improper type
434                                                // add error message
435                                                error = true
436                                                errors.rejectValue(
437                                                        'templateFileFields',
438                                                        'templateEntity.typeMismatch.file',
439                                                        [key, value.class] as Object[],
440                                                        'Property {0} must be of type String and is currently of type {1}'
441                                                )
442                                        }
443                                }
444                        }
445
446                        // validating required fields
447                        obj.getRequiredFields().findAll { it.type == TemplateFieldType.FILE }.each { field ->
448                                if (! fields.find { key, value -> key == field.name } ) {
449                                        // required field is missing
450                                        error = true
451                                        errors.rejectValue(
452                                                'templateFileFields',
453                                                'templateEntity.required',
454                                                [field.name] as Object[],
455                                                'Property {0} is required but it missing'
456                                        )
457                                }
458                        }
459
460                        // got an error, or not?
461                        return (!error)
462                })
463                templateBooleanFields(validator: { fields, obj, errors ->
464                        def error = false
465                        fields.each { key, value ->
466                                if (value) {
467                                        fields[key] = true;
468                                } else {
469                                        fields[key] = false;
470                                }
471                        }
472
473                        return (!error)
474                })
475                templateTemplateFields(validator: { fields, obj, errors ->
476                        def error = false
477                        fields.each { key, value ->
478                                if (value && value.class != Template) {
479                                        try {
480                                                fields[key] = (value as Template)
481                                        } catch (Exception e) {
482                                                error = true
483                                                errors.rejectValue(
484                                                        'templateTemplateFields',
485                                                        'templateEntity.typeMismatch.template',
486                                                        [key, value.class] as Object[],
487                                                        'Property {0} must be of type Template and is currently of type {1}'
488                                                )
489                                        }
490                                }
491                        }
492
493                        // validating required fields
494                        obj.getRequiredFields().findAll { it.type == TemplateFieldType.TEMPLATE }.each { field ->
495                                if (! fields.find { key, value -> key == field.name } ) {
496                                        // required field is missing
497                                        error = true
498                                        errors.rejectValue(
499                                                'templateTemplateFields',
500                                                'templateEntity.required',
501                                                [field.name] as Object[],
502                                                'Property {0} is required but it missing'
503                                        )
504                                }
505                        }
506
507                        return (!error)
508                })
509                templateModuleFields(validator: { fields, obj, errors ->
510                        def error = false
511                        fields.each { key, value ->
512                                if (value && value.class != AssayModule) {
513                                        try {
514                                                fields[key] = (value as AssayModule)
515                                        } catch (Exception e) {
516                                                error = true
517                                                errors.rejectValue(
518                                                        'templateModuleFields',
519                                                        'templateEntity.typeMismatch.module',
520                                                        [key, value.class] as Object[],
521                                                        'Property {0} must be of type AssayModule and is currently of type {1}'
522                                                )
523                                        }
524                                }
525                        }
526
527                        // validating required fields
528                        obj.getRequiredFields().findAll { it.type == TemplateFieldType.MODULE }.each { field ->
529                                if (! fields.find { key, value -> key == field.name } ) {
530                                        // required field is missing
531                                        error = true
532                                        errors.rejectValue(
533                                                'templateModuleFields',
534                                                'templateEntity.required',
535                                                [field.name] as Object[],
536                                                'Property {0} is required but it missing'
537                                        )
538                                }
539                        }
540
541                        return (!error)
542                })
543                templateLongFields(validator: { fields, obj, errors ->
544                        def error = false
545                        fields.each { key, value ->
546                                // Long field should accept Longs and Integers
547                                if (value && value.class != Long ) {
548                                        if( value.class == Integer ) {
549                                                fields[key] = value.toLong();
550                                        } else {
551                                                try {
552                                                        fields[key] = Long.parseLong(value.trim())
553                                                } catch (Exception e) {
554                                                        error = true
555                                                        errors.rejectValue(
556                                                                'templateLongFields',
557                                                                'templateEntity.typeMismatch.long',
558                                                                [key, value.class] as Object[],
559                                                                'Property {0} must be of type Long and is currently of type {1}'
560                                                        )
561                                                }
562                                        }
563                                }
564                        }
565
566                        // validating required fields
567                        obj.getRequiredFields().findAll { it.type == TemplateFieldType.LONG }.each { field ->
568                                if (! fields.find { key, value -> key == field.name } ) {
569                                        // required field is missing
570                                        error = true
571                                        errors.rejectValue(
572                                                'templateLongFields',
573                                                'templateEntity.required',
574                                                [field.name] as Object[],
575                                                'Property {0} is required but it missing'
576                                        )
577                                }
578                        }
579
580                        return (!error)
581                })
582        }
583
584        /**
585         * Get the proper templateFields Map for a specific field type
586         * @param TemplateFieldType
587         * @return pointer
588         * @visibility private
589         * @throws NoSuchFieldException
590         */
591        public Map getStore(TemplateFieldType fieldType) {
592                switch (fieldType) {
593                        case TemplateFieldType.STRING:
594                                return templateStringFields
595                        case TemplateFieldType.TEXT:
596                                return templateTextFields
597                        case TemplateFieldType.STRINGLIST:
598                                return templateStringListFields
599                        case TemplateFieldType.DATE:
600                                return templateDateFields
601                        case TemplateFieldType.RELTIME:
602                                return templateRelTimeFields
603                        case TemplateFieldType.FILE:
604                                return templateFileFields
605                        case TemplateFieldType.DOUBLE:
606                                return templateDoubleFields
607                        case TemplateFieldType.ONTOLOGYTERM:
608                                return templateTermFields
609                        case TemplateFieldType.BOOLEAN:
610                                return templateBooleanFields
611                        case TemplateFieldType.TEMPLATE:
612                                return templateTemplateFields
613                        case TemplateFieldType.MODULE:
614                                return templateModuleFields
615                        case TemplateFieldType.LONG:
616                                return templateLongFields
617                        default:
618                                throw new NoSuchFieldException("Field type ${fieldType} not recognized")
619                }
620        }
621
622        /**
623         * Find a field domain or template field by its name and return its description
624         * @param fieldsCollection the set of fields to search in, usually something like this.giveFields()
625         * @param fieldName The name of the domain or template field
626         * @return the TemplateField description of the field
627         * @throws NoSuchFieldException If the field is not found or the field type is not supported
628         */
629        private static TemplateField getField(List<TemplateField> fieldsCollection, String fieldName) {
630                // escape the fieldName for easy matching
631                // (such escaped names are commonly used
632                // in the HTTP forms of this application)
633                String escapedLowerCaseFieldName = fieldName.toLowerCase().replaceAll("([^a-z0-9])", "_")
634
635                // Find the target template field, if not found, throw an error
636                TemplateField field = fieldsCollection.find { it.name.toLowerCase().replaceAll("([^a-z0-9])", "_") == escapedLowerCaseFieldName }
637
638                if (field) {
639                        return field
640                }
641                else {
642                        throw new NoSuchFieldException("Field ${fieldName} not recognized")
643                }
644        }
645
646        /**
647         * Find a domain or template field by its name and return its value for this entity
648         * @param fieldName The name of the domain or template field
649         * @return the value of the field (class depends on the field type)
650         * @throws NoSuchFieldException If the field is not found or the field type is not supported
651         */
652        def getFieldValue(String fieldName) {
653
654                if (isDomainField(fieldName)) {
655                        return this[fieldName]
656                }
657                else {
658                        TemplateField field = getField(this.giveTemplateFields(), fieldName)
659                        return getStore(field.type)[fieldName]
660                }
661        }
662
663        /**
664         * Check whether a given template field exists or not
665         * @param fieldName The name of the template field
666         * @return true if the given field exists and false otherwise
667         */
668        boolean fieldExists(String fieldName) {
669                // getField should throw a NoSuchFieldException if the field does not exist
670                try {
671                        TemplateField field = getField(this.giveFields(), fieldName)
672                        // return true if exception is not thrown (but double check if field really is not null)
673                        if (field) {
674                                return true
675                        }
676                        else {
677                                return false
678                        }
679                }
680                // if exception is thrown, return false
681                catch (NoSuchFieldException e) {
682                        return false
683                }
684        }
685
686        /**
687         * Set a template/entity field value
688         * @param fieldName The name of the template or entity field
689         * @param value The value to be set, this should align with the (template) field type, but there are some convenience setters
690         */
691        def setFieldValue(String fieldName, value) {
692                // get the template field
693                TemplateField field = getField(this.giveFields(), fieldName)
694
695                // Convenience setter for boolean fields
696                if( field.type == TemplateFieldType.BOOLEAN && value && value.class == String ) {
697                        def lower = value.toLowerCase()
698                        if (lower.equals("true") || lower.equals("on") || lower.equals("x")) {
699                                value = true
700                        }
701                        else if (lower.equals("false") || lower.equals("off") || lower.equals("")) {
702                                value = false
703                        }
704                        else {
705                                throw new IllegalArgumentException("Boolean string not recognized: ${value} when setting field ${fieldName}")
706                        }
707                }
708
709                // Convenience setter for template string list fields: find TemplateFieldListItem by name
710                if (field.type == TemplateFieldType.STRINGLIST && value && value.class == String) {
711                        def escapedLowerCaseValue = value.toLowerCase().replaceAll("([^a-z0-9])", "_")
712                        value = field.listEntries.find {
713                                it.name.toLowerCase().replaceAll("([^a-z0-9])", "_") == escapedLowerCaseValue
714                        }
715                }
716
717                // Magic setter for dates: handle string values for date fields
718                if (field.type == TemplateFieldType.DATE && value && value.class == String) {
719                        // a string was given, attempt to transform it into a date instance
720                        // and -for now- assume the dd/mm/yyyy format
721                        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,})/
722                        if (dateMatch.matches()) {
723                                // create limited 'autosensing' datetime parser
724                                // assume dd mm yyyy  or dd mm yy
725                                def parser = 'd' + dateMatch[0][2] + 'M' + dateMatch[0][4] + (((dateMatch[0][5] as int) > 999) ? 'yyyy' : 'yy')
726
727                                // add time as well?
728                                if (dateMatch[0][7] != null) {
729                                        parser += dateMatch[0][8] + 'HH:mm'
730                                }
731
732                                value = new Date().parse(parser, value)
733                        }
734                }
735
736                // Magic setter for relative times: handle string values for relTime fields
737                //
738                if (field.type == TemplateFieldType.RELTIME && value != null && value.class == String) {
739                        // A string was given, attempt to transform it into a timespan
740                        // If it cannot be parsed, set the lowest possible value of Long.
741                        // The validator method will raise an error
742                        //
743                        // N.B. If you try to set Long.MIN_VALUE itself, an error will occur
744                        // However, this will never occur: this value represents 3 bilion centuries
745                        try {
746                                value = RelTime.parseRelTime(value).getValue();
747                        } catch (IllegalArgumentException e) {
748                                value = Long.MIN_VALUE;
749                        }
750                }
751
752                // Sometimes the fileService is not created yet
753                if (!fileService) {
754                        fileService = new FileService();
755                }
756
757                // Magic setter for files: handle values for file fields
758                //
759                // If NULL is given or "*deleted*", the field value is emptied and the old file is removed
760                // If an empty string is given, the field value is kept as was
761                // If a file is given, it is moved to the right directory. Old files are deleted. If
762                //   the file does not exist, the field is kept
763                // If a string is given, it is supposed to be a file in the upload directory. If
764                //   it is different from the old one, the old one is deleted. If the file does not
765                //   exist, the old one is kept.
766                if (field.type == TemplateFieldType.FILE) {
767                        def currentFile = getFieldValue(field.name);
768
769                        if (value == null || ( value.class == String && value == '*deleted*' ) ) {
770                                // If NULL is given, the field value is emptied and the old file is removed
771                                value = "";
772                                if (currentFile) {
773                                        fileService.delete(currentFile)
774                                }
775                        } else if (value.class == File) {
776                                // a file was given. Attempt to move it to the upload directory, and
777                                // afterwards, store the filename. If the file doesn't exist
778                                // or can't be moved, "" is returned
779                                value = fileService.moveFileToUploadDir(value);
780
781                                if (value) {
782                                        if (currentFile) {
783                                                fileService.delete(currentFile)
784                                        }
785                                } else {
786                                        value = currentFile;
787                                }
788                        } else if (value == "") {
789                                value = currentFile;
790                        } else {
791                                if (value != currentFile) {
792                                        if (fileService.fileExists(value)) {
793                                                // When a FILE field is filled, and a new file is set
794                                                // the existing file should be deleted
795                                                if (currentFile) {
796                                                        fileService.delete(currentFile)
797                                                }
798                                        } else {
799                                                // If the file does not exist, the field is kept
800                                                value = currentFile;
801                                        }
802                                }
803                        }
804                }
805
806                // Magic setter for ontology terms: handle string values
807                if (field.type == TemplateFieldType.ONTOLOGYTERM && value && value.class == String) {
808                        // iterate through ontologies and find term
809                        field.ontologies.each() { ontology ->
810                                // If we've found a term already, value.class == Term. In that case,
811                                // we shouldn't look further. Unfortunately, groovy doesn't support breaking out of
812                                // each(), so we check it on every iteration.
813                                if( value.class == String ) {
814                                        def term = ontology.giveTermByName(value)
815
816                                        // found a term?
817                                        if (term) {
818                                                value = term
819                                        }
820                                }
821                        }
822
823                        // If the term is not found in any ontology
824                        if( value.class == String ) {
825                                // TODO: search ontology for the term online (it may still exist) and insert it into the Term cache
826                                // if not found, throw exception
827                                throw new IllegalArgumentException("Ontology term not recognized (not in the GSCF ontology cache): ${value} when setting field ${fieldName}")
828                        }
829                }
830
831                // Magic setter for TEMPLATE fields
832                if (field.type == TemplateFieldType.TEMPLATE && value && value.class == String) {
833                        value = Template.findByName(value)
834                }
835
836                // Magic setter for MODULE fields
837                if (field.type == TemplateFieldType.MODULE && value && value.class == String) {
838                        value = AssayModule.findByName(value)
839                }
840
841                // Magic setter for LONG fields
842                if (field.type == TemplateFieldType.LONG && value && value.class == String) {
843                        // A check for invalidity is done in the validator of these fields. For that
844                        // reason, we just try to parse it here. If it fails, the validator will also
845                        // fail.
846                        try {
847                                value = Long.parseLong(value.trim())
848                        } catch( Exception e ) {}
849                }
850
851                // Set the field value
852                if (isDomainField(field)) {
853                        // got a value?
854                        if (value) {
855                                //log.debug ".setting [" + ((super) ? super.class : '??') + "] ("+getIdentifier()+") domain field: [" + fieldName + "] ([" + value.toString() + "] of type [" + value.class + "])"
856                                this[field.name] = value
857                        } else {
858                                //log.debug ".unsetting [" + ((super) ? super.class : '??') + "] ("+getIdentifier()+") domain field: [" + fieldName + "]"
859
860                                // remove value. For numbers, this is done by setting
861                                // the value to 0, otherwise, setting it to NULL
862                                switch (field.type.toString()) {
863                                        case [ 'DOUBLE', 'RELTIME', 'LONG']:
864                                                this[field.name] = 0;
865                                                break;
866                                        case [ 'BOOLEAN' ]:
867                                                this[field.name] = false;
868                                                break;
869                                        default:
870                                                this[field.name] = null
871                                }
872                        }
873                } else {
874                        // Caution: this assumes that all template...Field Maps are already initialized (as is done now above as [:])
875                        // If that is ever changed, the results are pretty much unpredictable (random Java object pointers?)!
876                        def store = getStore(field.type)
877
878                        // If some value is entered (or 0 or BOOLEAN false), then save the value
879                        // otherwise, it should not be present in the store, so
880                        // it is unset if it is.
881                        if (value || value == 0 || ( field.type == TemplateFieldType.BOOLEAN && value == false)) {
882                                //log.debug ".setting [" + ((super) ? super.class : '??') + "] ("+getIdentifier()+") template field: [" + fieldName + "] ([" + value.toString() + "] of type [" + value.class + "])"
883
884                                // set value
885                                store[fieldName] = value
886                        } else if (store[fieldName]) {
887                                //log.debug ".unsetting [" + ((super) ? super.class : '??') + "] ("+getIdentifier()+") template field: [" + fieldName + "]"
888
889                                // remove the item from the Map (if present)
890                                store.remove(fieldName)
891                        }
892                }
893
894                return this
895        }
896
897        /**
898         * Check if a given field is a domain field
899         * @param TemplateField field instance
900         * @return boolean
901         */
902        boolean isDomainField(TemplateField field) {
903                return isDomainField(field.name)
904        }
905
906        /**
907         * Check if a given field is a domain field
908         * @param String field name
909         * @return boolean
910         */
911        boolean isDomainField(String fieldName) {
912                return this.giveDomainFields()*.name.contains(fieldName)
913        }
914
915        /**
916         * Return all fields defined in the underlying template and the built-in
917         * domain fields of this entity
918         */
919        def List<TemplateField> giveFields() {
920                return this.giveDomainFields() + this.giveTemplateFields();
921        }
922
923        /**
924         * Return all templated fields defined in the underlying template of this entity
925         */
926        def List<TemplateField> giveTemplateFields() {
927                return (this.template) ? this.template.fields : []
928        }
929
930        def TemplateField getField( fieldName ) {
931                return getField(this.giveFields(), fieldName);
932        }
933
934        /**
935         * Look up the type of a certain template field
936         * @param String fieldName The name of the template field
937         * @return String The type (static member of TemplateFieldType) of the field, or null of the field does not exist
938         */
939        TemplateFieldType giveFieldType(String fieldName) {
940                def field = giveFields().find {
941                        it.name == fieldName
942                }
943                field?.type
944        }
945
946        /**
947         * Return all relevant 'built-in' domain fields of the super class. Should be implemented by a static method
948         * @return List with DomainTemplateFields
949         * @see TemplateField
950         */
951        abstract List<TemplateField> giveDomainFields()
952
953        /**
954         * Convenience method. Returns all unique templates used within a collection of TemplateEntities.
955         *
956         * If the collection is empty, an empty set is returned. If none of the entities contains
957         * a template, also an empty set is returned.
958         */
959        static Collection<Template> giveTemplates(Collection<TemplateEntity> entityCollection) {
960                def set = entityCollection*.template?.unique();
961
962                // If one or more entities does not have a template, the resulting
963                // set contains null. That is not what is meant.
964                set = set.findAll { it != null };
965
966                // Sort the list so we always have the same order
967                set = set.sort{ a, b ->
968                        a == null || b == null || a.equals(b) ? 0 :
969                        a.name < b.name ? -1 :
970                        a.name > b.name ?  1 :
971                        a.id < b.id ? -1 : 1
972                }
973
974                return set
975        }
976
977        /**
978         * Convenience method. Returns the template used within a collection of TemplateEntities.
979         * @throws NoSuchFieldException when 0 or multiple templates are used in the collection
980         * @return The template used by all members of a collection
981         */
982        static Template giveSingleTemplate(Collection<TemplateEntity> entityCollection) {
983                def templates = giveTemplates(entityCollection);
984                if (templates.size() == 0) {
985                        throw new NoSuchFieldException("No templates found in collection!")
986                } else if (templates.size() == 1) {
987                        return templates[0];
988                } else {
989                        throw new NoSuchFieldException("Multiple templates found in collection!")
990                }
991        }
992
993    /**
994     * Returns a Class object given by the entityname, but only if it is a subclass of TemplateEntity
995         *
996         * @return A class object of the given entity, null if the entity is not a subclass of TemplateEntity
997         * @throws ClassNotFoundException
998     */
999    static Class parseEntity( String entityName ) {
1000                if( entityName == null )
1001                        return null
1002
1003        // Find the templates
1004        def entity = Class.forName(entityName, true, Thread.currentThread().getContextClassLoader())
1005
1006        // succes, is entity an instance of TemplateEntity?
1007        if (entity?.superclass =~ /TemplateEntity$/ || entity?.superclass?.superclass =~ /TemplateEntity$/) {
1008            return entity;
1009        } else {
1010            return null;
1011        }
1012
1013    }
1014}
Note: See TracBrowser for help on using the browser.