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

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

Improvements in querying in order to be able to refine searches in a module.

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