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

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

Bugfix that solves a bug when searching as administrator

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