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

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

Refactored some of the querying stuff and built in 'check all' checkboxes

  • Property svn:keywords set to Rev Author Date
File size: 19.7 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: 1524 $
13 * $Author: robert@isdat.nl $
14 * $Date: 2011-02-15 14:05:23 +0000 (di, 15 feb 2011) $
15 */
16package dbnp.query
17
18import nl.grails.plugins.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
30import org.dbnp.gdt.*
31
32/**
33 * Available boolean operators for searches
34 * @author robert
35 *
36 */
37enum SearchMode {
38        and, or
39}
40
41class Search {
42        /**
43         * User that is performing this search. This has impact on the search results returned.
44         */
45        public SecUser user;
46
47        /**
48         * Date of execution of this search
49         */
50        public Date executionDate;
51
52        /**
53         * Public identifier of this search. Is only used when this query is saved in session
54         */
55        public int id;
56
57        /**
58         * Human readable entity name of the entities that can be found using this search
59         */
60        public String entity;
61
62        /**
63         * Mode to search: OR or AND.
64         * @see SearchMode
65         */
66        public SearchMode searchMode = SearchMode.and
67
68        protected List criteria;
69        protected List results;
70        protected Map resultFields = [:];
71
72        /**
73         * Returns a list of Criteria
74         */
75        public List getCriteria() { return criteria; }
76
77        /**
78         * Sets a new list of criteria
79         * @param c     List with criteria objects
80         */
81        public void setCriteria( List c ) { criteria = c; }
82
83        /**
84         * Adds a criterion to this query
85         * @param c     Criterion
86         */
87        public void addCriterion( Criterion c ) {
88                if( criteria )
89                        criteria << c;
90                else
91                        criteria = [c];
92        }
93
94        /**
95         * Retrieves the results found using this query. The result is empty is
96         * the query has not been executed yet.
97         */
98        public List getResults() { return results; }
99
100        /**
101         * Returns the results found using this query, filtered by a list of ids.
102         * @param selectedIds   List with ids of the entities you want to return.
103         * @return      A list with only those results for which the id is in the selectedIds
104         */
105        public List filterResults( List selectedIds ) {
106                if( !selectedIds || !results )
107                        return results
108
109                return results.findAll {
110                        selectedIds.contains( it.id )
111                }
112        }
113
114        /**
115         * Returns a list of fields for the results of this query. The fields returned are those
116         * fields that the query searched for.
117         */
118        public Map getResultFields() { return resultFields; }
119
120        /**
121         * Constructor of this search object. Sets the user field to the
122         * currently logged in user
123         * @see #user
124         */
125        public Search() {
126                def ctx = ApplicationHolder.getApplication().getMainContext();
127                def authenticationService = ctx.getBean("authenticationService");
128                def sessionUser = authenticationService?.getLoggedInUser();
129
130                if( sessionUser )
131                        this.user = sessionUser;
132                else
133                        this.user = null
134        }
135
136        /**
137         * Returns the number of results found by this search
138         * @return
139         */
140        public int getNumResults() {
141                if( results )
142                        return results.size();
143
144                return 0;
145        }
146
147        /**
148         * Executes a search based on the given criteria. Should be filled in by
149         * subclasses searching for a specific entity
150         *
151         * @param       c       List with criteria to search on
152         */
153        public void execute( List c ) {
154                setCriteria( c );
155                execute();
156        }
157
158        /**
159         * Executes a search based on the given criteria.
160         */
161        public void execute() {
162                this.executionDate = new Date();
163
164                switch( searchMode ) {
165                        case SearchMode.and:
166                                executeAnd();
167                                break;
168                        case SearchMode.or:
169                                executeOr();
170                                break;
171                }
172
173                // Save the value of this results for later use
174                saveResultFields();
175        }
176
177        /**
178         * Executes an inclusive (AND) search based on the given criteria. Should be filled in by
179         * subclasses searching for a specific entity
180         */
181        public void executeAnd() {
182
183        }
184
185        /**
186         * Executes an exclusive (OR) search based on the given criteria. Should be filled in by
187         * subclasses searching for a specific entity
188         */
189        public void executeOr() {
190
191        }
192
193        /************************************************************************
194         *
195         * These methods are used in querying and should be overridden by subclasses
196         * in order to provide custom searching
197         *
198         */
199
200        /**
201         * Returns a closure for the given entitytype that determines the value for a criterion
202         * on the given object. The closure receives two parameters: the object and a criterion.
203         *
204         * For example: when searching for studies, the object given to the closure is a Study.
205         * Also, when searching for samples, the object given is a Sample. When you have the criterion
206         *
207         *      sample.name equals 'sample 1'
208         *
209         * and searching for samples, it is easy to determine the value of the object for this criterion:
210         *     
211         *      object.getFieldValue( "name" )
212         *
213         *
214         * However, when searching for samples with the criterion
215         *
216         *      study.title contains 'nbic'
217         *
218         * this determination is more complex:
219         *
220         *      object.parent.getFieldValue( "title" )
221         *
222         *
223         * The other way around, when searching for studies with
224         *
225         *      sample.name equals 'sample 1'
226         *
227         * the value of the 'sample.name' property is a list:
228         *
229         *      object.samples*.getFieldValue( "name" )
230         * 
231         * The other search methods will handle the list and see whether any of the values
232         * matches the criterion.
233         *
234         * NB. The Criterion object has a convenience method to retrieve the field value on a
235         * specific (TemplateEntity) object: getFieldValue. This method also handles
236         * non-existing fields and casts the value to the correct type.
237         *
238         * This method should be overridden by all searches
239         *
240         * @see Criterion.getFieldValue()
241         *
242         * @return      Closure having 2 parameters: object and criterion
243         */
244        protected Closure valueCallback( String entity ) {
245                switch( entity ) {
246                        case "Study":
247                        case "Subject":
248                        case "Sample":
249                        case "Event":
250                        case "SamplingEvent":
251                        case "Assay":
252                                return { object, criterion -> return criterion.getFieldValue( object ); }
253                        default:
254                                return null;
255                }
256        }
257
258        /*****************************************************
259         *
260         * The other methods are helper functions for the execution of queries in subclasses
261         *
262         *****************************************************/
263
264        /**
265         * Returns a list of criteria targeted on the given entity
266         * @param entity        Entity to search criteria for
267         * @return                      List of criteria
268         */
269        protected List getEntityCriteria( String entity ) {
270                return criteria?.findAll { it.entity == entity }
271        }
272
273        /**
274         * Filters a list with entities, based on the given criteria and a closure to check whether a criterion is matched
275         *
276         * @param entities      Original list with entities to check for these criteria
277         * @param criteria      List with criteria to match on
278         * @param check         Closure to see whether a specific entity matches a criterion. Gets two arguments:
279         *                                              element         The element to check
280         *                                              criterion       The criterion to check on.
281         *                                      Returns true if the criterion holds, false otherwise
282         * @return                      The filtered list of entities
283         */
284        protected List filterEntityList( List entities, List<Criterion> criteria, Closure check ) {
285                if( !entities || !criteria || criteria.size() == 0 ) {
286                        if( searchMode == SearchMode.and )
287                                return entities;
288                        else if( searchMode == SearchMode.or )
289                                return []
290                }
291
292                return entities.findAll { entity ->
293                        if( searchMode == SearchMode.and ) {
294                                for( criterion in criteria ) {
295                                        if( !check( entity, criterion ) ) {
296                                                return false;
297                                        }
298                                }
299                                return true;
300                        } else if( searchMode == SearchMode.or ) {
301                                for( criterion in criteria ) {
302                                        if( check( entity, criterion ) ) {
303                                                return true;
304                                        }
305                                }
306                                return false;
307                        }
308                }
309        }
310
311        /**
312         * Prepares a value from a template entity for comparison, by giving it a correct type
313         *
314         * @param value         Value of the field
315         * @param type          TemplateFieldType       Type of the specific field
316         * @return                      The value of the field in the correct entity
317         */
318        public static def prepare( def value, TemplateFieldType type ) {
319                if( value == null )
320                        return value
321
322                switch (type) {
323                        case TemplateFieldType.DATE:
324                                try {
325                                        return new SimpleDateFormat( "yyyy-MM-dd" ).parse( value )
326                                } catch( Exception e ) {
327                                        return value.toString();
328                                }
329                        case TemplateFieldType.RELTIME:
330                                try {
331                                        if( value instanceof Number ) {
332                                                return new RelTime( value );
333                                        } else if( value.toString().isNumber() ) {
334                                                return new RelTime( Long.parseLong( value.toString() ) )
335                                        } else {
336                                                return new RelTime( value );
337                                        }
338                                } catch( Exception e ) {
339                                        try {
340                                                return Long.parseLong( value )
341                                        } catch( Exception e2 ) {
342                                                return value.toString();
343                                        }
344                                }
345                        case TemplateFieldType.DOUBLE:
346                                try {
347                                        return Double.valueOf( value )
348                                } catch( Exception e ) {
349                                        return value.toString();
350                                }
351                        case TemplateFieldType.BOOLEAN:
352                                try {
353                                        return Boolean.valueOf( value )
354                                } catch( Exception e ) {
355                                        return value.toString();
356                                }
357                        case TemplateFieldType.LONG:
358                                try {
359                                        return Long.valueOf( value )
360                                } catch( Exception e ) {
361                                        return value.toString();
362                                }
363                        case TemplateFieldType.STRING:
364                        case TemplateFieldType.TEXT:
365                        case TemplateFieldType.STRINGLIST:
366                        case TemplateFieldType.TEMPLATE:
367                        case TemplateFieldType.MODULE:
368                        case TemplateFieldType.FILE:
369                        case TemplateFieldType.ONTOLOGYTERM:
370                        default:
371                                return value.toString();
372                }
373
374        }
375
376        /**
377         * Filters the given list of studies on the study criteria
378         * @param studies               Original list of studies
379         * @param entity                Name of the entity to check the criteria for
380         * @param valueCallback Callback having a study and criterion as input, returning the value of the field to check on
381         * @return                              List with all studies that match the Criteria
382         */
383        protected List filterOnTemplateEntityCriteria( List studies, String entityName, Closure valueCallback ) {
384                def criteria = getEntityCriteria( entityName );
385
386                def checkCallback = { study, criterion ->
387                        def value = valueCallback( study, criterion );
388
389                        if( value == null ) {
390                                return false
391                        }
392
393                        if( value instanceof Collection ) {
394                                return criterion.matchAny( value )
395                        } else {
396                                return criterion.match( value );
397                        }
398                }
399
400                return filterEntityList( studies, criteria, checkCallback);
401        }
402
403        /**
404         * Filters the given list of studies on the study criteria
405         * @param studies       Original list of studies
406         * @return                      List with all studies that match the Study criteria
407         */
408        protected List filterOnStudyCriteria( List studies ) {
409                def entity = "Study"
410                return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
411        }
412
413        /**
414         * Filters the given list of studies on the subject criteria
415         * @param studies       Original list of studies
416         * @return                      List with all studies that match the Subject-criteria
417         */
418        protected List filterOnSubjectCriteria( List studies ) {
419                def entity = "Subject"
420                return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
421        }
422
423        /**
424         * Filters the given list of studies on the sample criteria
425         * @param studies       Original list of studies
426         * @return                      List with all studies that match the sample-criteria
427         */
428        protected List filterOnSampleCriteria( List studies ) {
429                def entity = "Sample"
430                return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
431        }
432
433        /**
434         * Filters the given list of studies on the event criteria
435         * @param studies       Original list of studies
436         * @return                      List with all studies that match the event-criteria
437         */
438        protected List filterOnEventCriteria( List studies ) {
439                def entity = "Event"
440                return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
441        }
442
443        /**
444         * Filters the given list of studies on the sampling event criteria
445         * @param studies       Original list of studies
446         * @return                      List with all studies that match the event-criteria
447         */
448        protected List filterOnSamplingEventCriteria( List studies ) {
449                def entity = "SamplingEvent"
450                return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
451        }
452
453        /**
454         * Filters the given list of studies on the assay criteria
455         * @param studies       Original list of studies
456         * @return                      List with all studies that match the assay-criteria
457         */
458        protected List filterOnAssayCriteria( List studies ) {
459                def entity = "Assay"
460                return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
461        }
462
463        /**
464         * Filters the given list of entities on the module criteria
465         * @param entities      Original list of entities. Entities should expose a giveUUID() method to give the token.
466         * @return                      List with all entities that match the module criteria
467         */
468        protected List filterOnModuleCriteria( List entities ) {
469                // An empty list can't be filtered more than is has been now
470                if( !entities || entities.size() == 0 )
471                        return [];
472
473                // Determine the moduleCommunicationService. Because this object
474                // is mocked in the tests, it can't be converted to a ApplicationContext object
475                def ctx = ApplicationHolder.getApplication().getMainContext();
476                def moduleCommunicationService = ctx.getBean("moduleCommunicationService");
477
478                switch( searchMode ) {
479                        case SearchMode.and:
480                                // Loop through all modules and check whether criteria have been given
481                                // for that module
482                                AssayModule.list().each { module ->
483                                        // Remove 'module' from module name
484                                        def moduleName = module.name.replace( 'module', '' ).trim()
485                                        def moduleCriteria = getEntityCriteria( moduleName );
486               
487                                        if( moduleCriteria && moduleCriteria.size() > 0 ) {
488                                                def callUrl = moduleCriteriaUrl( module, entities, moduleCriteria );
489                                               
490                                                try {
491                                                        def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl );
492                                                        Closure checkClosure = moduleCriterionClosure( json );
493                                                        entities = filterEntityList( entities, moduleCriteria, checkClosure );
494                                                } catch( Exception e ) {
495                                                        log.error( "Error while retrieving data from " + module.name + ": " + e.getMessage() )
496                                                }
497                                        }
498                                }
499               
500                                return entities;
501                        case SearchMode.or:
502                                def resultingEntities = []
503                               
504                                // Loop through all modules and check whether criteria have been given
505                                // for that module
506                                AssayModule.list().each { module ->
507                                        // Remove 'module' from module name
508                                        def moduleName = module.name.replace( 'module', '' ).trim()
509                                        def moduleCriteria = getEntityCriteria( moduleName );
510               
511                                        if( moduleCriteria && moduleCriteria.size() > 0 ) {
512                                                def callUrl = moduleCriteriaUrl( module, entities, moduleCriteria );
513                                               
514                                                try {
515                                                        def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl );
516                                                        Closure checkClosure = moduleCriterionClosure( json );
517                                                       
518                                                        resultingEntities += filterEntityList( entities, moduleCriteria, checkClosure );
519                                                        resultingEntities = resultingEntities.unique();
520                                                       
521                                                } catch( Exception e ) {
522                                                        log.error( "Error while retrieving data from " + module.name + ": " + e.getMessage() )
523                                                }
524                                        }
525                                }
526               
527                                println this.resultFields;
528                               
529                                return resultingEntities;
530                        default:
531                                return [];
532                }
533        }
534       
535        /**
536         * Returns a closure for determining the value of a module field
537         * @param json
538         * @return
539         */
540        protected Closure moduleCriterionClosure( def json ) {
541                return { entity, criterion ->
542                        // Find the value of the field in this sample. That value is still in the
543                        // JSON object
544                        def token = entity.giveUUID()
545                        if( !json[ token ] || json[ token ][ criterion.field ] == null )
546                                return false;
547
548                        // Check whether a list or string is given
549                        def value = json[ token ][ criterion.field ];
550
551                        // Save the value of this entity for later use
552                        saveResultField( entity.id, criterion.entity + " " + criterion.field, value )
553
554                        if( !( value instanceof Collection ) ) {
555                                value = [ value ];
556                        }
557
558                        // Convert numbers to a long or double in order to process them correctly
559                        def values = value.collect { val ->
560                                val = val.toString();
561                                if( val.isLong() ) {
562                                        val = Long.parseLong( val );
563                                } else if( val.isDouble() ) {
564                                        val = Double.parseDouble( val );
565                                }
566                                return val;
567                        }
568
569                        // Loop through all values and match any
570                        for( val in values ) {
571                                if( criterion.match( val ) )
572                                        return true;
573                        }
574
575                        return false;
576                }
577        }
578       
579        protected String moduleCriteriaUrl( module, entities, moduleCriteria ) {
580                // Retrieve the data from the module
581                def tokens = entities.collect { it.giveUUID() }.unique();
582                def fields = moduleCriteria.collect { it.field }.unique();
583       
584                def callUrl = module.url + '/rest/getQueryableFieldData?entity=' + this.entity
585                tokens.sort().each { callUrl += "&tokens=" + it.encodeAsURL() }
586                fields.sort().each { callUrl += "&fields=" + it.encodeAsURL() }
587
588                return callUrl;
589        }
590
591        /*********************************************************************
592         *
593         * These methods are used for saving information about the search results and showing the information later on.
594         *
595         *********************************************************************/
596
597        /**
598         * Saves data about template entities to use later on. This data is copied to a special
599         * structure to make it compatible with data fetched from other modules.
600         * @see #saveResultField()
601         */
602        protected void saveResultFields() {
603                if( !results || !criteria )
604                        return
605
606                criteria.each { criterion ->
607                        if( criterion.field ) {
608                                def valueCallback = valueCallback( criterion.entity );
609                               
610                                if( valueCallback != null ) {
611                                        def name = criterion.entity + ' ' + criterion.field
612       
613                                        results.each { result ->
614                                                saveResultField( result.id, name, valueCallback( result, criterion ) );
615                                        }
616                                }
617                        }
618                }
619        }
620
621        /**
622         * Saves data about template entities to use later on. This data is copied to a special
623         * structure to make it compatible with data fetched from other modules.
624         * @param entities                      List of template entities to find data in
625         * @param criteria                      Criteria to search for
626         * @param valueCallback         Callback to retrieve a specific field from the entity
627         * @see #saveResultField()
628         */
629        protected void saveResultFields( entities, criteria, valueCallback ) {
630                for( criterion in criteria ) {
631                        for( entity in entities ) {
632                                if( criterion.field )
633                                        saveResultField( entity.id, criterion.entity + ' ' + criterion.field, valueCallback( entity, criterion ) )
634                        }
635                }
636        }
637
638
639        /**
640         * Saves a specific field of an object to use later on. Especially useful when looking up data from other modules.
641         * @param id            ID of the object
642         * @param fieldName     Field name that has been searched
643         * @param value         Value of the field
644         */
645        protected void saveResultField( id, fieldName, value ) {
646                if( resultFields[ id ] == null )
647                        resultFields[ id ] = [:]
648
649                // Handle special cases
650                if( value == null )
651                        value = "";
652
653                if( value instanceof Collection ) {
654                        value = value.findAll { it != null }
655                }
656
657                resultFields[ id ][ fieldName ] = value;
658        }
659
660        /**
661         * Removes all data from the result field map
662         */
663        protected void clearResultFields() {
664                resultFields = [:]
665        }
666
667        /**
668         * 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.
669         *
670         * 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
671         * query.)
672         * @return      Map with the entity id as a key, and a field-value map as value
673         */
674        public Map getShowableResultFields() {
675                def resultIds = getResults()*.id;
676                return getResultFields().findAll {
677                        resultIds.contains( it.key )
678                }
679        }
680       
681        /**
682         * Returns the field names that are found in the map with showable result fields
683         *
684         * @param fields        Map with showable result fields
685         * @see getShowableResultFields
686         * @return
687         */
688        public List getShowableResultFieldNames( fields ) {
689                return fields.values()*.keySet().flatten().unique();
690        }
691
692        public String toString() {
693                return ( this.entity ? this.entity + " search" : "Search" ) + " " + this.id
694        }
695}
Note: See TracBrowser for help on using the repository browser.