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

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