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

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

Resolving issues with searching (#446)

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