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

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

Fixed a bug for case-insensitive search in 'complex' fields

  • 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: 1904 $
13 * $Author: robert@isdat.nl $
14 * $Date: 2011-05-31 10:25:51 +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 ) {
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                                if( entities ) {
349                                        def paramName = from.replaceAll( /\W/, '' );
350                                        fullHQL.where << sprintf( entityClause( entityName ), from, alias, paramName );
351                                        fullHQL.parameters[ paramName ] = entities
352                                        return true;
353                                } 
354                        }
355                       
356                        // No results are found.
357                        results = [];
358                        return false
359                }
360               
361                return true;
362        }
363       
364        /**
365         * Add all conditions for a wildcard search (all fields in a given entity)
366         * @param fullHQL       Original HQL map to be extended (fields 'from', 'where' and 'parameters')
367         * @return                      True if the addition worked
368         */
369        protected boolean addWildcardConditions( def fullHQL, def entities) {
370                // Append study criteria
371                def entityCriteria = getEntityCriteria( "*" );
372               
373                // If no wildcard criteria are found, return immediately
374                if( !entityCriteria )
375                        return true
376                       
377                // Wildcards should be checked within each entity
378                def wildcardHQL = createHQLForEntity( this.entity, null, false );
379               
380                // Create SQL for other entities, by executing a subquery first, and
381                // afterwards selecting the study based on the entities found
382                entities.each { entityToSearch ->
383                        // Add conditions for all criteria for the given entity. However,
384                        // the conditions for the 'main' entity (the entity being sought) are already added
385                        if( entity != entityToSearch ) {
386                                addEntityConditions(
387                                        entityToSearch,                                                                                                                 // Name of the entity to search in
388                                        TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entityToSearch ),  // Class of the entity to search in
389                                        elementName( entityToSearch ),                                                                                  // HQL name of the collection to search in
390                                        entityToSearch[0].toLowerCase() + entityToSearch[1..-1],                                // Alias for the entity to search in
391                                        wildcardHQL,                                                                                                                    // Current HQL statement
392                                        entityCriteria                                                                                                                  // Only create HQL for these criteria
393                                )
394                        }
395                }
396               
397                // Add these clauses to the full HQL statement
398                def whereClauses = wildcardHQL.where.findAll { it };
399
400                if( whereClauses ) {
401                        fullHQL.from += wildcardHQL.from
402                        fullHQL.where << whereClauses.join( " OR " )
403                         
404                        wildcardHQL[ "parameters" ].each {
405                                fullHQL.parameters[ it.key ] = it.value
406                        }
407                }
408               
409                return true;
410        }
411       
412        /**
413         * Create HQL statement for the given criteria and a specific entity
414         * @param entityName            Name of the entity
415         * @param entityCriteria        (optional) list of criteria to create the HQL for. If no criteria are given, all criteria for the entity are found
416         * @param includeFrom           (optional) If set to true, the 'FROM entity' is prepended to the from clause. Defaults to true
417         * @return
418         */
419        def createHQLForEntity( String entityName, def entityCriteria = null, includeFrom = true ) {
420                def fromClause = includeFrom ? "FROM " + entityName + " " + entityName.toLowerCase() : ""
421                def whereClause = []
422                def parameters = [:]
423                def criterionNum = 0;
424               
425                // Append study criteria
426                if( entityCriteria == null )
427                        entityCriteria = getEntityCriteria( entityName );
428               
429                entityCriteria.each {
430                        def criteriaHQL = it.toHQL( "criterion" +entityName + criterionNum++, entityName.toLowerCase() );
431                        fromClause += " " + criteriaHQL[ "join" ]
432                        whereClause << criteriaHQL[ "where" ]
433                        criteriaHQL[ "parameters" ].each {
434                                parameters[ it.key ] = it.value
435                        }
436                }
437               
438                // Add a filter such that only readable studies are returned
439                if( entityName == "Study" ) {
440                       
441                        if( this.user == null ) {
442                                // Anonymous readers are only given access when published and public
443                                whereClause << "( study.publicstudy = true AND study.published = true )"
444                        } else if( !this.user.hasAdminRights() ) {
445                                // Administrators are allowed to read every study
446
447                                // Owners and writers are allowed to read this study
448                                // Readers are allowed to read this study when it is published
449                                whereClause << "( study.owner = :sessionUser OR :sessionUser member of study.writers OR ( :sessionUser member of study.readers AND study.published = true ) )"
450                                parameters[ "sessionUser" ] = this.user
451                        }
452                }
453               
454                return [ "from": fromClause, "where": whereClause, "parameters": parameters ]
455        }
456       
457        /*****************************************************
458         *
459         * The other methods are helper functions for the execution of queries in subclasses
460         *
461         *****************************************************/
462
463        /**
464         * Returns a list of criteria targeted on the given entity
465         * @param entity        Entity to search criteria for
466         * @return                      List of criteria
467         */
468        protected List getEntityCriteria( String entity ) {
469                return criteria?.findAll { it.entity == entity }
470        }
471       
472       
473        /**
474         * Prepares a value from a template entity for comparison, by giving it a correct type
475         *
476         * @param value         Value of the field
477         * @param type          TemplateFieldType       Type of the specific field
478         * @return                      The value of the field in the correct entity
479         */
480        public static def prepare( def value, TemplateFieldType type ) {
481                if( value == null )
482                        return value
483
484                switch (type) {
485                        case TemplateFieldType.DATE:
486                                try {
487                                        return new SimpleDateFormat( "yyyy-MM-dd" ).parse( value.toString() )
488                                } catch( Exception e ) {
489                                        return value.toString();
490                                }
491                        case TemplateFieldType.RELTIME:
492                                try {
493                                        if( value instanceof Number ) {
494                                                return new RelTime( value );
495                                        } else if( value.toString().isNumber() ) {
496                                                return new RelTime( Long.parseLong( value.toString() ) )
497                                        } else {
498                                                return new RelTime( value );
499                                        }
500                                } catch( Exception e ) {
501                                        try {
502                                                return Long.parseLong( value )
503                                        } catch( Exception e2 ) {
504                                                return value.toString();
505                                        }
506                                }
507                        case TemplateFieldType.DOUBLE:
508                                try {
509                                        return Double.valueOf( value )
510                                } catch( Exception e ) {
511                                        return value.toString();
512                                }
513                        case TemplateFieldType.BOOLEAN:
514                                try {
515                                        return Boolean.valueOf( value )
516                                } catch( Exception e ) {
517                                        return value.toString();
518                                }
519                        case TemplateFieldType.LONG:
520                                try {
521                                        return Long.valueOf( value )
522                                } catch( Exception e ) {
523                                        return value.toString();
524                                }
525                        case TemplateFieldType.STRING:
526                        case TemplateFieldType.TEXT:
527                        case TemplateFieldType.STRINGLIST:
528                        case TemplateFieldType.TEMPLATE:
529                        case TemplateFieldType.MODULE:
530                        case TemplateFieldType.FILE:
531                        case TemplateFieldType.ONTOLOGYTERM:
532                        default:
533                                return value.toString();
534                }
535
536        }
537
538        /*****************************************************
539        *
540        * Methods for filtering lists based on specific (GSCF) criteria
541        *
542        *****************************************************/
543
544       
545        /**
546         * Filters a list with entities, based on the given criteria and a closure to check whether a criterion is matched
547         *
548         * @param entities      Original list with entities to check for these criteria
549         * @param criteria      List with criteria to match on
550         * @param check         Closure to see whether a specific entity matches a criterion. Gets two arguments:
551         *                                              element         The element to check
552         *                                              criterion       The criterion to check on.
553         *                                      Returns true if the criterion holds, false otherwise
554         * @return                      The filtered list of entities
555         */
556        protected List filterEntityList( List entities, List<Criterion> criteria, Closure check ) {
557                if( !entities || !criteria || criteria.size() == 0 ) {
558                        if( searchMode == SearchMode.and )
559                                return entities;
560                        else if( searchMode == SearchMode.or )
561                                return []
562                }
563
564                return entities.findAll { entity ->
565                        if( searchMode == SearchMode.and ) {
566                                for( criterion in criteria ) {
567                                        if( !check( entity, criterion ) ) {
568                                                return false;
569                                        }
570                                }
571                                return true;
572                        } else if( searchMode == SearchMode.or ) {
573                                for( criterion in criteria ) {
574                                        if( check( entity, criterion ) ) {
575                                                return true;
576                                        }
577                                }
578                                return false;
579                        }
580                }
581        }
582       
583        /**
584         * Filters an entity list manually on complex criteria found in the criteria list.
585         * This method is needed because hibernate contains a bug in the HQL INDEX() function.
586         * See also Criterion.manyToManyWhereCondition and
587         * http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615
588         *
589         * @param entities                      List of entities
590         * @param entityCriteria        List of criteria that apply to the type of entities given       (e.g. Subject criteria for Subjects)
591         * @return                                      Filtered entity list
592         */
593        protected filterForComplexCriteria( def entities, def entityCriteria ) {
594                def complexCriteria = entityCriteria.findAll { it.isComplexCriterion() }
595               
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.