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

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

Fixed a bug in wildcard searches: now the system also searches through domain fields, instead of only looking in template fields

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