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

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

Bugfix for querying without criteria

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