source: trunk/src/groovy/dbnp/query/Search.groovy @ 1822

Last change on this file since 1822 was 1822, checked in by robert@…, 10 years ago

Bugfix for wildcard searches

  • Property svn:keywords set to Rev Author Date
File size: 30.6 KB
Line 
1/**
2 * Search Domain Class
3 *
4 * Abstract class containing search criteria and search results when querying.
5 * Should be subclassed in order to enable searching for different entities.
6 *
7 * @author  Robert Horlings (robert@isdat.nl)
8 * @since       20110118
9 * @package     dbnp.query
10 *
11 * Revision information:
12 * $Rev: 1822 $
13 * $Author: robert@isdat.nl $
14 * $Date: 2011-05-09 08:40:02 +0000 (ma, 09 mei 2011) $
15 */
16package dbnp.query
17
18import org.dbnp.gdt.*
19import java.util.List;
20import java.text.DateFormat;
21import java.text.SimpleDateFormat
22import java.util.List;
23
24import org.springframework.context.ApplicationContext
25import org.springframework.web.context.request.RequestContextHolder;
26import org.codehaus.groovy.grails.commons.ApplicationHolder;
27
28import dbnp.authentication.*
29
30/**
31 * Available boolean operators for searches
32 * @author robert
33 *
34 */
35enum SearchMode {
36        and, or
37}
38
39class Search {
40        /**
41         * User that is performing this search. This has impact on the search results returned.
42         */
43        public SecUser user;
44
45        /**
46         * Date of execution of this search
47         */
48        public Date executionDate;
49
50        /**
51         * Public identifier of this search. Is only used when this query is saved in session
52         */
53        public int id;
54
55        /**
56         * Human readable entity name of the entities that can be found using this search
57         */
58        public String entity;
59
60        /**
61         * Mode to search: OR or AND.
62         * @see SearchMode
63         */
64        public SearchMode searchMode = SearchMode.and
65
66        protected List criteria;
67        protected List results;
68        protected Map resultFields = [:];
69
70        /**
71         * Constructor of this search object. Sets the user field to the
72         * currently logged in user
73         * @see #user
74         */
75        public Search() {
76                def ctx = ApplicationHolder.getApplication().getMainContext();
77                def authenticationService = ctx.getBean("authenticationService");
78                def sessionUser = authenticationService?.getLoggedInUser();
79
80                if( sessionUser )
81                        this.user = sessionUser;
82                else
83                        this.user = null
84        }
85
86        /**
87         * Returns the number of results found by this search
88         * @return
89         */
90        public int getNumResults() {
91                if( results )
92                        return results.size();
93
94                return 0;
95        }
96
97        /**
98         * Executes a search based on the given criteria. Should be filled in by
99         * subclasses searching for a specific entity
100         *
101         * @param       c       List with criteria to search on
102         */
103        public void execute( List c ) {
104                setCriteria( c );
105                execute();
106        }
107
108        /**
109         * Executes a search based on the given criteria.
110         */
111        public void execute() {
112                this.executionDate = new Date();
113
114                // Execute the search
115                executeSearch();
116
117                // Save the value of this results for later use
118                saveResultFields();
119        }
120
121        /**
122         * Executes a query
123         */
124        protected void executeSearch() {
125                // Create HQL query for criteria for the entity being sought
126                def selectClause = "" 
127                def fullHQL = createHQLForEntity( this.entity );
128
129                // Create SQL for other entities, by executing a subquery first, and
130                // afterwards selecting the study based on the entities found
131                def resultsFound
132
133                def entityNames = [ "Study", "Subject", "Sample", "Assay", "Event", "SamplingEvent" ];
134                for( entityToSearch in entityNames ) {
135                        // Add conditions for all criteria for the given entity. However,
136                        // the conditions for the 'main' entity (the entity being sought) are already added
137                        if( entity != entityToSearch ) {
138                                resultsFound = addEntityConditions( 
139                                        entityToSearch,                                                                                                                 // Name of the entity to search in
140                                        TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entityToSearch ),  // Class of the entity to search in
141                                        elementName( entityToSearch ),                                                                                  // HQL name of the collection to search in
142                                        entityToSearch[0].toLowerCase() + entityToSearch[1..-1],                                // Alias for the entity to search in
143                                        fullHQL                                                                                                                                 // Current HQL statement
144                                )
145                               
146                                // If no results are found, and we are searching 'inclusive', there will be no
147                                // results whatsoever. So we can quit this method now.
148                                if( !resultsFound && searchMode == SearchMode.and ) {
149                                        return
150                                }
151                        }
152                }
153               
154                println fullHQL;
155               
156                // Search in all entities
157                resultsFound = addWildcardConditions( fullHQL, entityNames )
158                if( !resultsFound && searchMode == SearchMode.and ) {
159                        return
160                }
161               
162                println fullHQL;
163
164                // Combine all parts to generate a full HQL query
165                def hqlQuery = selectClause + " " + fullHQL.from + ( fullHQL.where ? "  WHERE " + fullHQL.where.join( " " + searchMode.toString() + " "  ) : "" );
166               
167                // Find all objects
168                def entities = entityClass().findAll( hqlQuery, fullHQL.parameters );
169               
170                // Find criteria that match one or more 'complex' fields
171                // These criteria must be checked extra, since they are not correctly handled
172                // by the HQL criteria. See also Criterion.manyToManyWhereCondition and
173                // http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615
174                entities = filterForComplexCriteria( entities, getEntityCriteria( this.entity ) );
175               
176                // Filter on module criteria. If the search is 'and', only the entities found until now
177                // should be queried in the module. Otherwise, all entities are sent, in order to retrieve
178                // data (to show on screen) for all entities
179                if( hasModuleCriteria() ) {
180                        if( searchMode == SearchMode.and ) {
181                                entities = filterOnModuleCriteria( entities );
182                        } else {
183                                entities = filterOnModuleCriteria( entityClass().list().findAll { this.isAccessible( it ) } )
184                        }
185                }
186               
187                // Determine which studies can be read
188                results = entities;
189               
190        }
191               
192        /************************************************************************
193         *
194         * These methods are used in querying and can be overridden by subclasses
195         * in order to provide custom searching
196         *
197         ************************************************************************/
198
199        /**
200         * Returns a closure for the given entitytype that determines the value for a criterion
201         * on the given object. The closure receives two parameters: the object and a criterion.
202         *
203         * For example: when searching for studies, the object given to the closure is a Study.
204         * Also, when searching for samples, the object given is a Sample. When you have the criterion
205         *
206         *      sample.name equals 'sample 1'
207         *
208         * and searching for samples, it is easy to determine the value of the object for this criterion:
209         *     
210         *      object.getFieldValue( "name" )
211         *
212         *
213         * However, when searching for samples with the criterion
214         *
215         *      study.title contains 'nbic'
216         *
217         * this determination is more complex:
218         *
219         *      object.parent.getFieldValue( "title" )
220         *
221         *
222         * The other way around, when searching for studies with
223         *
224         *      sample.name equals 'sample 1'
225         *
226         * the value of the 'sample.name' property is a list:
227         *
228         *      object.samples*.getFieldValue( "name" )
229         * 
230         * The other search methods will handle the list and see whether any of the values
231         * matches the criterion.
232         *
233         * NB. The Criterion object has a convenience method to retrieve the field value on a
234         * specific (TemplateEntity) object: getFieldValue. This method also handles
235         * non-existing fields and casts the value to the correct type.
236         *
237         * This method should be overridden by all searches
238         *
239         * @see Criterion.getFieldValue()
240         *
241         * @return      Closure having 2 parameters: object and criterion
242         */
243        protected Closure valueCallback( String entity ) {
244                switch( entity ) {
245                        case "Study":
246                        case "Subject":
247                        case "Sample":
248                        case "Event":
249                        case "SamplingEvent":
250                        case "Assay":
251                                return { object, criterion -> return criterion.getFieldValue( object ); }
252                        default:
253                                return null;
254                }
255        }
256       
257        /**
258        * Returns the HQL name for the element or collections to be searched in, for the given entity name
259        * For example: when searching for Subject.age > 50 with Study results, the system must search in all study.subjects for age > 50.
260        * But when searching for Sample results, the system must search in sample.parentSubject for age > 50
261        *
262        * This method should be overridden in child classes
263        *
264        * @param entity Name of the entity of the criterion
265        * @return                       HQL name for this element or collection of elements
266        */
267   protected String elementName( String entity ) {
268           switch( entity ) {
269                   case "Study":                       
270                   case "Subject":                     
271                   case "Sample":                       
272                   case "Event":                       
273                   case "SamplingEvent":       
274                   case "Assay":                       
275                                return entity[ 0 ].toLowerCase() + entity[ 1 .. -1 ]
276                   default:                             return null;
277           }
278   }
279   
280        /**
281        * Returns the a where clause for the given entity name
282        * For example: when searching for Subject.age > 50 with Study results, the system must search
283        *               
284        *       WHERE EXISTS( FROM study.subjects subject WHERE subject IN (...)
285        *
286        * The returned string is fed to sprintf with 3 string parameters:
287        *               from (in this case 'study.subjects'
288        *               alias (in this case 'subject'
289        *               paramName (in this case '...')
290        *
291        * This method can be overridden in child classes to enable specific behaviour
292        *
293        * @param entity         Name of the entity of the criterion
294        * @return                       HQL where clause for this element or collection of elements
295        */
296   protected String entityClause( String entity ) {
297           return ' EXISTS( FROM %1$s %2$s WHERE %2$s IN (:%3$s) )'
298   }
299   
300   /**
301    * Returns true iff the given entity is accessible by the user currently logged in
302    *
303    * This method should be overridden in child classes, since the check is different for every type of search
304    *
305    * @param entity             Entity to determine accessibility for. The entity is of the type 'this.entity'
306    * @return                   True iff the user is allowed to access this entity
307    */
308   protected boolean isAccessible( def entity ) {
309           return false
310   }
311
312        /****************************************************
313         *
314         * Helper methods for generating HQL statements
315         *
316         ****************************************************/
317       
318        /**
319         * Add all conditions for criteria for a specific entity
320         *
321         * @param entityName    Name of the entity to search in
322         * @param entityClass   Class of the entity to search
323         * @param from                  Name of the HQL collection to search in (e.g. study.subjects)
324         * @param alias                 Alias of the HQL collection objects (e.g. 'subject')
325         * @param fullHQL               Original HQL map to be extended (fields 'from', 'where' and 'parameters')
326         * @param determineParentId     Closure to determine the id of the final entity to search, based on these objects
327         * @param entityCriteria        (optional) list of criteria to create the HQL for. If no criteria are given, all criteria for the entity are found
328         * @return                              True if one ore more entities are found, false otherwise
329         */
330        protected boolean addEntityConditions( String entityName, def entityClass, String from, String alias, def fullHQL, def entityCriteria = null ) {
331                if( entityCriteria == null )
332                        entityCriteria = getEntityCriteria( entityName )
333               
334                // Create HQL for these criteria
335                def entityHQL = createHQLForEntity( entityName, entityCriteria );
336               
337                // If any clauses are generated for these criteria, find entities that match these criteria
338                def whereClauses = entityHQL.where?.findAll { it && it?.trim() != "" }
339                if( whereClauses ) {
340                        // First find all entities that match these criteria
341                        def hqlQuery = entityHQL.from + " WHERE " + whereClauses.join( searchMode == SearchMode.and ? " AND " : " OR " );                       
342                        def entities = entityClass.findAll( hqlQuery, entityHQL.parameters )
343                       
344                        // If there are entities matching these criteria, put a where clause in the full HQL query
345                        if( entities ) {
346                                // Find criteria that match one or more 'complex' fields
347                                // These criteria must be checked extra, since they are not correctly handled
348                                // by the HQL criteria. See also Criterion.manyToManyWhereCondition and
349                                // http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615
350                                entities = filterForComplexCriteria( entities, entityCriteria );
351                               
352                                def paramName = from.replaceAll( /\W/, '' );
353                                fullHQL.where << sprintf( entityClause( entityName ), from, alias, paramName );
354                                fullHQL.parameters[ paramName ] = entities
355                                return true;
356                        } else {
357                                results = [];
358                                return false
359                        }
360                }
361               
362                return true;
363        }
364       
365        /**
366         * Add all conditions for a wildcard search (all fields in a given entity)
367         * @param fullHQL       Original HQL map to be extended (fields 'from', 'where' and 'parameters')
368         * @return                      True if the addition worked
369         */
370        protected boolean addWildcardConditions( def fullHQL, def entities) {
371                // Append study criteria
372                def entityCriteria = getEntityCriteria( "*" );
373               
374                // If no wildcard criteria are found, return immediately
375                if( !entityCriteria )
376                        return true
377                       
378                // Wildcards should be checked within each entity
379                def wildcardHQL = createHQLForEntity( this.entity, null, false );
380               
381                // Create SQL for other entities, by executing a subquery first, and
382                // afterwards selecting the study based on the entities found
383                entities.each { entityToSearch ->
384                        // Add conditions for all criteria for the given entity. However,
385                        // the conditions for the 'main' entity (the entity being sought) are already added
386                        if( entity != entityToSearch ) {
387                                addEntityConditions(
388                                        entityToSearch,                                                                                                                 // Name of the entity to search in
389                                        TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entityToSearch ),  // Class of the entity to search in
390                                        elementName( entityToSearch ),                                                                                  // HQL name of the collection to search in
391                                        entityToSearch[0].toLowerCase() + entityToSearch[1..-1],                                // Alias for the entity to search in
392                                        wildcardHQL,                                                                                                                    // Current HQL statement
393                                        entityCriteria                                                                                                                  // Only create HQL for these criteria
394                                )
395                        }
396                }
397               
398                // Add these clauses to the full HQL statement
399                def whereClauses = wildcardHQL.where.findAll { it };
400
401                if( whereClauses ) {
402                        fullHQL.from += wildcardHQL.from
403                        fullHQL.where << whereClauses.join( " OR " )
404                         
405                        wildcardHQL[ "parameters" ].each {
406                                fullHQL.parameters[ it.key ] = it.value
407                        }
408                }
409               
410                return true;
411        }
412       
413        /**
414         * Create HQL statement for the given criteria and a specific entity
415         * @param entityName            Name of the entity
416         * @param entityCriteria        (optional) list of criteria to create the HQL for. If no criteria are given, all criteria for the entity are found
417         * @param includeFrom           (optional) If set to true, the 'FROM entity' is prepended to the from clause. Defaults to true
418         * @return
419         */
420        def createHQLForEntity( String entityName, def entityCriteria = null, includeFrom = true ) {
421                def fromClause = includeFrom ? "FROM " + entityName + " " + entityName.toLowerCase() : ""
422                def whereClause = []
423                def parameters = [:]
424                def criterionNum = 0;
425               
426                // Append study criteria
427                if( entityCriteria == null )
428                        entityCriteria = getEntityCriteria( entityName );
429               
430                entityCriteria.each {
431                        def criteriaHQL = it.toHQL( "criterion" +entityName + criterionNum++, entityName.toLowerCase() );
432                        fromClause += " " + criteriaHQL[ "join" ]
433                        whereClause << criteriaHQL[ "where" ]
434                        criteriaHQL[ "parameters" ].each {
435                                parameters[ it.key ] = it.value
436                        }
437                }
438               
439                // Add a filter such that only readable studies are returned
440                if( entityName == "Study" ) {
441                       
442                        if( this.user == null ) {
443                                // Anonymous readers are only given access when published and public
444                                whereClause << "( study.publicstudy = true AND study.published = true )"
445                        } else if( !this.user.hasAdminRights() ) {
446                                // Administrators are allowed to read every study
447
448                                // Owners and writers are allowed to read this study
449                                // Readers are allowed to read this study when it is published
450                                whereClause << "( study.owner = :sessionUser OR :sessionUser member of study.writers OR ( :sessionUser member of study.readers AND study.published = true ) )"
451                                parameters[ "sessionUser" ] = this.user
452                        }
453                }
454               
455                return [ "from": fromClause, "where": whereClause, "parameters": parameters ]
456        }
457       
458        /*****************************************************
459         *
460         * The other methods are helper functions for the execution of queries in subclasses
461         *
462         *****************************************************/
463
464        /**
465         * Returns a list of criteria targeted on the given entity
466         * @param entity        Entity to search criteria for
467         * @return                      List of criteria
468         */
469        protected List getEntityCriteria( String entity ) {
470                return criteria?.findAll { it.entity == entity }
471        }
472       
473       
474        /**
475         * Prepares a value from a template entity for comparison, by giving it a correct type
476         *
477         * @param value         Value of the field
478         * @param type          TemplateFieldType       Type of the specific field
479         * @return                      The value of the field in the correct entity
480         */
481        public static def prepare( def value, TemplateFieldType type ) {
482                if( value == null )
483                        return value
484
485                switch (type) {
486                        case TemplateFieldType.DATE:
487                                try {
488                                        return new SimpleDateFormat( "yyyy-MM-dd" ).parse( value.toString() )
489                                } catch( Exception e ) {
490                                        return value.toString();
491                                }
492                        case TemplateFieldType.RELTIME:
493                                try {
494                                        if( value instanceof Number ) {
495                                                return new RelTime( value );
496                                        } else if( value.toString().isNumber() ) {
497                                                return new RelTime( Long.parseLong( value.toString() ) )
498                                        } else {
499                                                return new RelTime( value );
500                                        }
501                                } catch( Exception e ) {
502                                        try {
503                                                return Long.parseLong( value )
504                                        } catch( Exception e2 ) {
505                                                return value.toString();
506                                        }
507                                }
508                        case TemplateFieldType.DOUBLE:
509                                try {
510                                        return Double.valueOf( value )
511                                } catch( Exception e ) {
512                                        return value.toString();
513                                }
514                        case TemplateFieldType.BOOLEAN:
515                                try {
516                                        return Boolean.valueOf( value )
517                                } catch( Exception e ) {
518                                        return value.toString();
519                                }
520                        case TemplateFieldType.LONG:
521                                try {
522                                        return Long.valueOf( value )
523                                } catch( Exception e ) {
524                                        return value.toString();
525                                }
526                        case TemplateFieldType.STRING:
527                        case TemplateFieldType.TEXT:
528                        case TemplateFieldType.STRINGLIST:
529                        case TemplateFieldType.TEMPLATE:
530                        case TemplateFieldType.MODULE:
531                        case TemplateFieldType.FILE:
532                        case TemplateFieldType.ONTOLOGYTERM:
533                        default:
534                                return value.toString();
535                }
536
537        }
538
539        /*****************************************************
540        *
541        * Methods for filtering lists based on specific (GSCF) criteria
542        *
543        *****************************************************/
544
545       
546        /**
547         * Filters a list with entities, based on the given criteria and a closure to check whether a criterion is matched
548         *
549         * @param entities      Original list with entities to check for these criteria
550         * @param criteria      List with criteria to match on
551         * @param check         Closure to see whether a specific entity matches a criterion. Gets two arguments:
552         *                                              element         The element to check
553         *                                              criterion       The criterion to check on.
554         *                                      Returns true if the criterion holds, false otherwise
555         * @return                      The filtered list of entities
556         */
557        protected List filterEntityList( List entities, List<Criterion> criteria, Closure check ) {
558                if( !entities || !criteria || criteria.size() == 0 ) {
559                        if( searchMode == SearchMode.and )
560                                return entities;
561                        else if( searchMode == SearchMode.or )
562                                return []
563                }
564
565                return entities.findAll { entity ->
566                        if( searchMode == SearchMode.and ) {
567                                for( criterion in criteria ) {
568                                        if( !check( entity, criterion ) ) {
569                                                return false;
570                                        }
571                                }
572                                return true;
573                        } else if( searchMode == SearchMode.or ) {
574                                for( criterion in criteria ) {
575                                        if( check( entity, criterion ) ) {
576                                                return true;
577                                        }
578                                }
579                                return false;
580                        }
581                }
582        }
583       
584        /**
585         * Filters an entity list manually on complex criteria found in the criteria list.
586         * This method is needed because hibernate contains a bug in the HQL INDEX() function.
587         * See also Criterion.manyToManyWhereCondition and
588         *http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615
589         *
590         * @param entities                      List of entities
591         * @param entityCriteria        List of criteria that apply to the type of entities given       (e.g. Subject criteria for Subjects)
592         * @return                                      Filtered entity list
593         */
594        protected filterForComplexCriteria( def entities, def entityCriteria ) {
595                def complexCriteria = entityCriteria.findAll { it.isComplexCriterion() }
596                if( complexCriteria ) {
597                        def checkCallback = { entity, criterion ->
598                                def value = criterion.getFieldValue( entity )
599
600                                if( value == null ) {
601                                        return false
602                                }
603
604                                if( value instanceof Collection ) {
605                                        return value.any { criterion.match( it ) }
606                                } else {
607                                        return criterion.match( value );
608                                }
609                        }
610
611                        entities = filterEntityList( entities, complexCriteria, checkCallback );
612                }
613               
614                return entities;
615        }
616
617        /********************************************************************
618         *
619         * Methods for filtering object lists on module criteria
620         *
621         ********************************************************************/
622
623        protected boolean hasModuleCriteria() {
624               
625                return AssayModule.list().any { module ->
626                        // Remove 'module' from module name
627                        def moduleName = module.name.replace( 'module', '' ).trim()
628                        def moduleCriteria = getEntityCriteria( moduleName );
629                        return moduleCriteria?.size() > 0
630                }
631        }
632       
633        /**
634         * Filters the given list of entities on the module criteria
635         * @param entities      Original list of entities. Entities should expose a giveUUID() method to give the token.
636         * @return                      List with all entities that match the module criteria
637         */
638        protected List filterOnModuleCriteria( List entities ) {
639                // An empty list can't be filtered more than is has been now
640                if( !entities || entities.size() == 0 )
641                        return [];
642
643                // Determine the moduleCommunicationService. Because this object
644                // is mocked in the tests, it can't be converted to a ApplicationContext object
645                def ctx = ApplicationHolder.getApplication().getMainContext();
646                def moduleCommunicationService = ctx.getBean("moduleCommunicationService");
647
648                switch( searchMode ) {
649                        case SearchMode.and:
650                                // Loop through all modules and check whether criteria have been given
651                                // for that module
652                                AssayModule.list().each { module ->
653                                        // Remove 'module' from module name
654                                        def moduleName = module.name.replace( 'module', '' ).trim()
655                                        def moduleCriteria = getEntityCriteria( moduleName );
656               
657                                        if( moduleCriteria && moduleCriteria.size() > 0 ) {
658                                                def callUrl = moduleCriteriaUrl( module );
659                                                def callArgs = moduleCriteriaArguments( module, entities, moduleCriteria );
660                                               
661                                                try {
662                                                        def json = moduleCommunicationService.callModuleMethod( module.url, callUrl, callArgs, "POST" );
663                                                        Closure checkClosure = moduleCriterionClosure( json );
664                                                        entities = filterEntityList( entities, moduleCriteria, checkClosure );
665                                                } catch( Exception e ) {
666                                                        //log.error( "Error while retrieving data from " + module.name + ": " + e.getMessage() )
667                                                        e.printStackTrace()
668                                                        throw e
669                                                }
670                                        }
671                                }
672               
673                                return entities;
674                        case SearchMode.or:
675                                def resultingEntities = []
676                               
677                                // Loop through all modules and check whether criteria have been given
678                                // for that module
679                                AssayModule.list().each { module ->
680                                        // Remove 'module' from module name
681                                        def moduleName = module.name.replace( 'module', '' ).trim()
682                                        def moduleCriteria = getEntityCriteria( moduleName );
683               
684                                        if( moduleCriteria && moduleCriteria.size() > 0 ) {
685                                                def callUrl = moduleCriteriaUrl( module );
686                                                def callArgs = moduleCriteriaArguments( module, entities, moduleCriteria );
687                                               
688                                                try {
689                                                        def json = moduleCommunicationService.callModuleMethod( module.url, callUrl, callArgs, "POST" );
690                                                        Closure checkClosure = moduleCriterionClosure( json );
691                                                       
692                                                        resultingEntities += filterEntityList( entities, moduleCriteria, checkClosure );
693                                                        resultingEntities = resultingEntities.unique();
694                                                       
695                                                } catch( Exception e ) {
696                                                        //log.error( "Error while retrieving data from " + module.name + ": " + e.getMessage() )
697                                                        e.printStackTrace()
698                                                        throw e
699                                                }
700                                        }
701                                }
702               
703                                return resultingEntities;
704                        default:
705                                return [];
706                }
707        }
708       
709        /**
710         * Returns a closure for determining the value of a module field
711         * @param json
712         * @return
713         */
714        protected Closure moduleCriterionClosure( def json ) {
715                return { entity, criterion ->
716                        // Find the value of the field in this sample. That value is still in the
717                        // JSON object
718                        def token = entity.giveUUID()
719                        def value
720                       
721                        if( criterion.field == '*' ) {
722                                // Collect the values from all fields
723                                value = [];
724                                json[ token ].each { field ->
725                                        if( field.value instanceof Collection ) {
726                                                field.value.each { value << it }
727                                        } else {
728                                                value << field.value;
729                                        }
730                                }
731                        } else {
732                                if( !json[ token ] || json[ token ][ criterion.field ] == null )
733                                        return false;
734       
735                                // Check whether a list or string is given
736                                value = json[ token ][ criterion.field ];
737       
738                                // Save the value of this entity for later use
739                                saveResultField( entity.id, criterion.entity + " " + criterion.field, value )
740       
741                                if( !( value instanceof Collection ) ) {
742                                        value = [ value ];
743                                }
744                        }
745
746                        // Convert numbers to a long or double in order to process them correctly
747                        def values = value.collect { val ->
748                                val = val.toString();
749                                if( val.isLong() ) {
750                                        val = Long.parseLong( val );
751                                } else if( val.isDouble() ) {
752                                        val = Double.parseDouble( val );
753                                }
754                                return val;
755                        }
756
757                        // Loop through all values and match any
758                        for( val in values ) {
759                                if( criterion.match( val ) )
760                                        return true;
761                        }
762
763                        return false;
764                }
765        }
766       
767        protected String moduleCriteriaUrl( module ) {
768                def callUrl = module.url + '/rest/getQueryableFieldData'
769                return callUrl;
770        }
771       
772        protected String moduleCriteriaArguments( module, entities, moduleCriteria ) {
773                // Retrieve the data from the module
774                def tokens = entities.collect { it.giveUUID() }.unique();
775                def fields = moduleCriteria.collect { it.field }.unique();
776       
777                def callUrl = 'entity=' + this.entity
778                tokens.sort().each { callUrl += "&tokens=" + it.encodeAsURL() }
779               
780                // If all fields are searched, all fields should be retrieved
781                if( fields.contains( '*' ) ) {
782                       
783                } else {
784                        fields.sort().each { callUrl += "&fields=" + it.encodeAsURL() }
785                }
786
787                return callUrl;
788        }
789
790        /*********************************************************************
791         *
792         * These methods are used for saving information about the search results and showing the information later on.
793         *
794         *********************************************************************/
795
796        /**
797         * Saves data about template entities to use later on. This data is copied to a special
798         * structure to make it compatible with data fetched from other modules.
799         * @see #saveResultField()
800         */
801        protected void saveResultFields() {
802                if( !results || !criteria )
803                        return
804
805                criteria.each { criterion ->
806                        if( criterion.field && criterion.field != '*' ) {
807                                def valueCallback = valueCallback( criterion.entity );
808                               
809                                if( valueCallback != null ) {
810                                        def name = criterion.entity + ' ' + criterion.field
811       
812                                        results.each { result ->
813                                                saveResultField( result.id, name, valueCallback( result, criterion ) );
814                                        }
815                                }
816                        }
817                }
818        }
819
820        /**
821         * Saves data about template entities to use later on. This data is copied to a special
822         * structure to make it compatible with data fetched from other modules.
823         * @param entities                      List of template entities to find data in
824         * @param criteria                      Criteria to search for
825         * @param valueCallback         Callback to retrieve a specific field from the entity
826         * @see #saveResultField()
827         */
828        protected void saveResultFields( entities, criteria, valueCallback ) {
829                for( criterion in criteria ) {
830                        for( entity in entities ) {
831                                if( criterion.field && criterion.field != '*' )
832                                        saveResultField( entity.id, criterion.entity + ' ' + criterion.field, valueCallback( entity, criterion ) )
833                        }
834                }
835        }
836
837
838        /**
839         * Saves a specific field of an object to use later on. Especially useful when looking up data from other modules.
840         * @param id            ID of the object
841         * @param fieldName     Field name that has been searched
842         * @param value         Value of the field
843         */
844        protected void saveResultField( id, fieldName, value ) {
845                if( resultFields[ id ] == null )
846                        resultFields[ id ] = [:]
847
848                // Handle special cases
849                if( value == null )
850                        value = "";
851               
852                if( fieldName == "*" )
853                        return;
854                       
855                if( value instanceof Collection ) {
856                        value = value.findAll { it != null }
857                }
858               
859                resultFields[ id ][ fieldName ] = value;
860        }
861
862        /**
863         * Removes all data from the result field map
864         */
865        protected void clearResultFields() {
866                resultFields = [:]
867        }
868
869        /**
870         * Returns the saved field data that could be shown on screen. This means, the data is filtered to show only data of the query results.
871         *
872         * Subclasses could filter out the fields they don't want to show on the result screen (e.g. because they are shown regardless of the
873         * query.)
874         * @return      Map with the entity id as a key, and a field-value map as value
875         */
876        public Map getShowableResultFields() {
877                def resultIds = getResults()*.id;
878                return getResultFields().findAll {
879                        resultIds.contains( it.key )
880                }
881        }
882       
883        /**
884         * Returns the field names that are found in the map with showable result fields
885         *
886         * @param fields        Map with showable result fields
887         * @see getShowableResultFields
888         * @return
889         */
890        public List getShowableResultFieldNames( fields ) {
891                return fields.values()*.keySet().flatten().unique();
892        }
893
894       
895        /************************************************************************
896         *
897         * Getters and setters
898         *
899         ************************************************************************/
900       
901        /**
902        * Returns a list of Criteria
903        */
904   public List getCriteria() { return criteria; }
905
906   /**
907        * Sets a new list of criteria
908        * @param c      List with criteria objects
909        */
910   public void setCriteria( List c ) { criteria = c; }
911
912   /**
913        * Adds a criterion to this query
914        * @param c      Criterion
915        */
916   public void addCriterion( Criterion c ) {
917           if( criteria )
918                   criteria << c;
919           else
920                   criteria = [c];
921   }
922
923   /**
924        * Retrieves the results found using this query. The result is empty is
925        * the query has not been executed yet.
926        */
927   public List getResults() { return results; }
928
929   /**
930        * Returns the results found using this query, filtered by a list of ids.
931        * @param selectedIds    List with ids of the entities you want to return.
932        * @return       A list with only those results for which the id is in the selectedIds
933        */
934   public List filterResults( List selectedTokens ) {
935           if( !selectedTokens || !results )
936                   return results
937
938           return results.findAll {
939                   selectedTokens.contains( it.giveUUID() )
940           }
941   }
942
943   /**
944        * Returns a list of fields for the results of this query. The fields returned are those
945        * fields that the query searched for.
946        */
947   public Map getResultFields() { return resultFields; }
948       
949        public String toString() {
950                return ( this.entity ? this.entity + " search" : "Search" ) + " " + this.id
951        }
952       
953        public boolean equals( Object o ) {
954                if( o == null )
955                        return false
956               
957                if( !( o instanceof Search ) ) 
958                        return false
959                       
960                Search s = (Search) o;
961               
962                return (        searchMode              == s.searchMode && 
963                                        entity                  == s.entity && 
964                                        criteria.size() == s.criteria.size() && 
965                                        s.criteria.containsAll( criteria ) && 
966                                        criteria.containsAll( s.criteria ) );
967        }
968       
969        /**
970        * Returns the class for the entity being searched
971        * @return
972        */
973        public Class entityClass() {
974                if( !this.entity )
975                        return null;
976                       
977                try {
978                        return TemplateEntity.parseEntity( 'dbnp.studycapturing.' + this.entity)
979                } catch( Exception e ) {
980                        throw new Exception( "Unknown entity for criterion " + this, e );
981                }
982        }
983       
984}
Note: See TracBrowser for help on using the repository browser.