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

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

Adjusted querying process in order to improve speed with large amounts of data. The querying is now performed directly in HQL, instead of fetching all possible objects from the database and filtering them.

  • Property svn:keywords set to Rev Author Date
File size: 30.5 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: 1820 $
13 * $Author: robert@isdat.nl $
14 * $Date: 2011-05-06 15:41:49 +0000 (vr, 06 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                                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 );
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                if( complexCriteria ) {
593                        def checkCallback = { entity, criterion ->
594                                def value = criterion.getFieldValue( entity )
595
596                                if( value == null ) {
597                                        return false
598                                }
599
600                                if( value instanceof Collection ) {
601                                        return criterion.matchAny( value )
602                                } else {
603                                        return criterion.match( value );
604                                }
605                        }
606
607                        entities = filterEntityList( entities, complexCriteria, checkCallback );
608                }
609               
610                return entities;
611        }
612
613        /********************************************************************
614         *
615         * Methods for filtering object lists on module criteria
616         *
617         ********************************************************************/
618
619        protected boolean hasModuleCriteria() {
620               
621                return AssayModule.list().any { module ->
622                        // Remove 'module' from module name
623                        def moduleName = module.name.replace( 'module', '' ).trim()
624                        def moduleCriteria = getEntityCriteria( moduleName );
625                        return moduleCriteria?.size() > 0
626                }
627        }
628       
629        /**
630         * Filters the given list of entities on the module criteria
631         * @param entities      Original list of entities. Entities should expose a giveUUID() method to give the token.
632         * @return                      List with all entities that match the module criteria
633         */
634        protected List filterOnModuleCriteria( List entities ) {
635                // An empty list can't be filtered more than is has been now
636                if( !entities || entities.size() == 0 )
637                        return [];
638
639                // Determine the moduleCommunicationService. Because this object
640                // is mocked in the tests, it can't be converted to a ApplicationContext object
641                def ctx = ApplicationHolder.getApplication().getMainContext();
642                def moduleCommunicationService = ctx.getBean("moduleCommunicationService");
643
644                switch( searchMode ) {
645                        case SearchMode.and:
646                                // Loop through all modules and check whether criteria have been given
647                                // for that module
648                                AssayModule.list().each { module ->
649                                        // Remove 'module' from module name
650                                        def moduleName = module.name.replace( 'module', '' ).trim()
651                                        def moduleCriteria = getEntityCriteria( moduleName );
652               
653                                        if( moduleCriteria && moduleCriteria.size() > 0 ) {
654                                                def callUrl = moduleCriteriaUrl( module );
655                                                def callArgs = moduleCriteriaArguments( module, entities, moduleCriteria );
656                                               
657                                                try {
658                                                        def json = moduleCommunicationService.callModuleMethod( module.url, callUrl, callArgs, "POST" );
659                                                        Closure checkClosure = moduleCriterionClosure( json );
660                                                        entities = filterEntityList( entities, moduleCriteria, checkClosure );
661                                                } catch( Exception e ) {
662                                                        //log.error( "Error while retrieving data from " + module.name + ": " + e.getMessage() )
663                                                        e.printStackTrace()
664                                                        throw e
665                                                }
666                                        }
667                                }
668               
669                                return entities;
670                        case SearchMode.or:
671                                def resultingEntities = []
672                               
673                                // Loop through all modules and check whether criteria have been given
674                                // for that module
675                                AssayModule.list().each { module ->
676                                        // Remove 'module' from module name
677                                        def moduleName = module.name.replace( 'module', '' ).trim()
678                                        def moduleCriteria = getEntityCriteria( moduleName );
679               
680                                        if( moduleCriteria && moduleCriteria.size() > 0 ) {
681                                                def callUrl = moduleCriteriaUrl( module );
682                                                def callArgs = moduleCriteriaArguments( module, entities, moduleCriteria );
683                                               
684                                                try {
685                                                        def json = moduleCommunicationService.callModuleMethod( module.url, callUrl, callArgs, "POST" );
686                                                        Closure checkClosure = moduleCriterionClosure( json );
687                                                       
688                                                        resultingEntities += filterEntityList( entities, moduleCriteria, checkClosure );
689                                                        resultingEntities = resultingEntities.unique();
690                                                       
691                                                } catch( Exception e ) {
692                                                        //log.error( "Error while retrieving data from " + module.name + ": " + e.getMessage() )
693                                                        e.printStackTrace()
694                                                        throw e
695                                                }
696                                        }
697                                }
698               
699                                return resultingEntities;
700                        default:
701                                return [];
702                }
703        }
704       
705        /**
706         * Returns a closure for determining the value of a module field
707         * @param json
708         * @return
709         */
710        protected Closure moduleCriterionClosure( def json ) {
711                return { entity, criterion ->
712                        // Find the value of the field in this sample. That value is still in the
713                        // JSON object
714                        def token = entity.giveUUID()
715                        def value
716                       
717                        if( criterion.field == '*' ) {
718                                // Collect the values from all fields
719                                value = [];
720                                json[ token ].each { field ->
721                                        if( field.value instanceof Collection ) {
722                                                field.value.each { value << it }
723                                        } else {
724                                                value << field.value;
725                                        }
726                                }
727                        } else {
728                                if( !json[ token ] || json[ token ][ criterion.field ] == null )
729                                        return false;
730       
731                                // Check whether a list or string is given
732                                value = json[ token ][ criterion.field ];
733       
734                                // Save the value of this entity for later use
735                                saveResultField( entity.id, criterion.entity + " " + criterion.field, value )
736       
737                                if( !( value instanceof Collection ) ) {
738                                        value = [ value ];
739                                }
740                        }
741
742                        // Convert numbers to a long or double in order to process them correctly
743                        def values = value.collect { val ->
744                                val = val.toString();
745                                if( val.isLong() ) {
746                                        val = Long.parseLong( val );
747                                } else if( val.isDouble() ) {
748                                        val = Double.parseDouble( val );
749                                }
750                                return val;
751                        }
752
753                        // Loop through all values and match any
754                        for( val in values ) {
755                                if( criterion.match( val ) )
756                                        return true;
757                        }
758
759                        return false;
760                }
761        }
762       
763        protected String moduleCriteriaUrl( module ) {
764                def callUrl = module.url + '/rest/getQueryableFieldData'
765                return callUrl;
766        }
767       
768        protected String moduleCriteriaArguments( module, entities, moduleCriteria ) {
769                // Retrieve the data from the module
770                def tokens = entities.collect { it.giveUUID() }.unique();
771                def fields = moduleCriteria.collect { it.field }.unique();
772       
773                def callUrl = 'entity=' + this.entity
774                tokens.sort().each { callUrl += "&tokens=" + it.encodeAsURL() }
775               
776                // If all fields are searched, all fields should be retrieved
777                if( fields.contains( '*' ) ) {
778                       
779                } else {
780                        fields.sort().each { callUrl += "&fields=" + it.encodeAsURL() }
781                }
782
783                return callUrl;
784        }
785
786        /*********************************************************************
787         *
788         * These methods are used for saving information about the search results and showing the information later on.
789         *
790         *********************************************************************/
791
792        /**
793         * Saves data about template entities to use later on. This data is copied to a special
794         * structure to make it compatible with data fetched from other modules.
795         * @see #saveResultField()
796         */
797        protected void saveResultFields() {
798                if( !results || !criteria )
799                        return
800
801                criteria.each { criterion ->
802                        if( criterion.field && criterion.field != '*' ) {
803                                def valueCallback = valueCallback( criterion.entity );
804                               
805                                if( valueCallback != null ) {
806                                        def name = criterion.entity + ' ' + criterion.field
807       
808                                        results.each { result ->
809                                                saveResultField( result.id, name, valueCallback( result, criterion ) );
810                                        }
811                                }
812                        }
813                }
814        }
815
816        /**
817         * Saves data about template entities to use later on. This data is copied to a special
818         * structure to make it compatible with data fetched from other modules.
819         * @param entities                      List of template entities to find data in
820         * @param criteria                      Criteria to search for
821         * @param valueCallback         Callback to retrieve a specific field from the entity
822         * @see #saveResultField()
823         */
824        protected void saveResultFields( entities, criteria, valueCallback ) {
825                for( criterion in criteria ) {
826                        for( entity in entities ) {
827                                if( criterion.field && criterion.field != '*' )
828                                        saveResultField( entity.id, criterion.entity + ' ' + criterion.field, valueCallback( entity, criterion ) )
829                        }
830                }
831        }
832
833
834        /**
835         * Saves a specific field of an object to use later on. Especially useful when looking up data from other modules.
836         * @param id            ID of the object
837         * @param fieldName     Field name that has been searched
838         * @param value         Value of the field
839         */
840        protected void saveResultField( id, fieldName, value ) {
841                if( resultFields[ id ] == null )
842                        resultFields[ id ] = [:]
843
844                // Handle special cases
845                if( value == null )
846                        value = "";
847               
848                if( fieldName == "*" )
849                        return;
850                       
851                if( value instanceof Collection ) {
852                        value = value.findAll { it != null }
853                }
854               
855                resultFields[ id ][ fieldName ] = value;
856        }
857
858        /**
859         * Removes all data from the result field map
860         */
861        protected void clearResultFields() {
862                resultFields = [:]
863        }
864
865        /**
866         * 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.
867         *
868         * 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
869         * query.)
870         * @return      Map with the entity id as a key, and a field-value map as value
871         */
872        public Map getShowableResultFields() {
873                def resultIds = getResults()*.id;
874                return getResultFields().findAll {
875                        resultIds.contains( it.key )
876                }
877        }
878       
879        /**
880         * Returns the field names that are found in the map with showable result fields
881         *
882         * @param fields        Map with showable result fields
883         * @see getShowableResultFields
884         * @return
885         */
886        public List getShowableResultFieldNames( fields ) {
887                return fields.values()*.keySet().flatten().unique();
888        }
889
890       
891        /************************************************************************
892         *
893         * Getters and setters
894         *
895         ************************************************************************/
896       
897        /**
898        * Returns a list of Criteria
899        */
900   public List getCriteria() { return criteria; }
901
902   /**
903        * Sets a new list of criteria
904        * @param c      List with criteria objects
905        */
906   public void setCriteria( List c ) { criteria = c; }
907
908   /**
909        * Adds a criterion to this query
910        * @param c      Criterion
911        */
912   public void addCriterion( Criterion c ) {
913           if( criteria )
914                   criteria << c;
915           else
916                   criteria = [c];
917   }
918
919   /**
920        * Retrieves the results found using this query. The result is empty is
921        * the query has not been executed yet.
922        */
923   public List getResults() { return results; }
924
925   /**
926        * Returns the results found using this query, filtered by a list of ids.
927        * @param selectedIds    List with ids of the entities you want to return.
928        * @return       A list with only those results for which the id is in the selectedIds
929        */
930   public List filterResults( List selectedTokens ) {
931           if( !selectedTokens || !results )
932                   return results
933
934           return results.findAll {
935                   selectedTokens.contains( it.giveUUID() )
936           }
937   }
938
939   /**
940        * Returns a list of fields for the results of this query. The fields returned are those
941        * fields that the query searched for.
942        */
943   public Map getResultFields() { return resultFields; }
944       
945        public String toString() {
946                return ( this.entity ? this.entity + " search" : "Search" ) + " " + this.id
947        }
948       
949        public boolean equals( Object o ) {
950                if( o == null )
951                        return false
952               
953                if( !( o instanceof Search ) ) 
954                        return false
955                       
956                Search s = (Search) o;
957               
958                return (        searchMode              == s.searchMode && 
959                                        entity                  == s.entity && 
960                                        criteria.size() == s.criteria.size() && 
961                                        s.criteria.containsAll( criteria ) && 
962                                        criteria.containsAll( s.criteria ) );
963        }
964       
965        /**
966        * Returns the class for the entity being searched
967        * @return
968        */
969        public Class entityClass() {
970                if( !this.entity )
971                        return null;
972                       
973                try {
974                        return TemplateEntity.parseEntity( 'dbnp.studycapturing.' + this.entity)
975                } catch( Exception e ) {
976                        throw new Exception( "Unknown entity for criterion " + this, e );
977                }
978        }
979       
980}
Note: See TracBrowser for help on using the repository browser.