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

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

Added assay search and improved query form

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