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

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

Solved #344 - retrieving module data is now done using POST

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