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

Last change on this file since 1590 was 1590, checked in by work@…, 7 years ago
  • fixing remaining test issues
  • 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: 1590 $
13 * $Author: work@osx.eu $
14 * $Date: 2011-03-04 15:28:45 +0000 (vr, 04 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, entities, moduleCriteria );
520                                               
521                                                try {
522                                                        def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl );
523                                                        Closure checkClosure = moduleCriterionClosure( json );
524                                                        entities = filterEntityList( entities, moduleCriteria, checkClosure );
525                                                } catch( Exception e ) {
526                                                        log.error( "Error while retrieving data from " + module.name + ": " + e.getMessage() )
527                                                }
528                                        }
529                                }
530               
531                                return entities;
532                        case SearchMode.or:
533                                def resultingEntities = []
534                               
535                                // Loop through all modules and check whether criteria have been given
536                                // for that module
537                                AssayModule.list().each { module ->
538                                        // Remove 'module' from module name
539                                        def moduleName = module.name.replace( 'module', '' ).trim()
540                                        def moduleCriteria = getEntityCriteria( moduleName );
541               
542                                        if( moduleCriteria && moduleCriteria.size() > 0 ) {
543                                                def callUrl = moduleCriteriaUrl( module, entities, moduleCriteria );
544                                               
545                                                try {
546                                                        def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl );
547                                                        Closure checkClosure = moduleCriterionClosure( json );
548                                                       
549                                                        resultingEntities += filterEntityList( entities, moduleCriteria, checkClosure );
550                                                        resultingEntities = resultingEntities.unique();
551                                                       
552                                                } catch( Exception e ) {
553                                                        log.error( "Error while retrieving data from " + module.name + ": " + e.getMessage() )
554                                                }
555                                        }
556                                }
557               
558                                println this.resultFields;
559                               
560                                return resultingEntities;
561                        default:
562                                return [];
563                }
564        }
565       
566        /**
567         * Returns a closure for determining the value of a module field
568         * @param json
569         * @return
570         */
571        protected Closure moduleCriterionClosure( def json ) {
572                return { entity, criterion ->
573                        // Find the value of the field in this sample. That value is still in the
574                        // JSON object
575                        def token = entity.giveUUID()
576                        if( !json[ token ] || json[ token ][ criterion.field ] == null )
577                                return false;
578
579                        // Check whether a list or string is given
580                        def value = json[ token ][ criterion.field ];
581
582                        // Save the value of this entity for later use
583                        saveResultField( entity.id, criterion.entity + " " + criterion.field, value )
584
585                        if( !( value instanceof Collection ) ) {
586                                value = [ value ];
587                        }
588
589                        // Convert numbers to a long or double in order to process them correctly
590                        def values = value.collect { val ->
591                                val = val.toString();
592                                if( val.isLong() ) {
593                                        val = Long.parseLong( val );
594                                } else if( val.isDouble() ) {
595                                        val = Double.parseDouble( val );
596                                }
597                                return val;
598                        }
599
600                        // Loop through all values and match any
601                        for( val in values ) {
602                                if( criterion.match( val ) )
603                                        return true;
604                        }
605
606                        return false;
607                }
608        }
609       
610        protected String moduleCriteriaUrl( module, entities, moduleCriteria ) {
611                // Retrieve the data from the module
612                def tokens = entities.collect { it.giveUUID() }.unique();
613                def fields = moduleCriteria.collect { it.field }.unique();
614       
615                def callUrl = module.url + '/rest/getQueryableFieldData?entity=' + this.entity
616                tokens.sort().each { callUrl += "&tokens=" + it.encodeAsURL() }
617                fields.sort().each { callUrl += "&fields=" + it.encodeAsURL() }
618
619                return callUrl;
620        }
621
622        /*********************************************************************
623         *
624         * These methods are used for saving information about the search results and showing the information later on.
625         *
626         *********************************************************************/
627
628        /**
629         * Saves data about template entities to use later on. This data is copied to a special
630         * structure to make it compatible with data fetched from other modules.
631         * @see #saveResultField()
632         */
633        protected void saveResultFields() {
634                if( !results || !criteria )
635                        return
636
637                criteria.each { criterion ->
638                        if( criterion.field ) {
639                                def valueCallback = valueCallback( criterion.entity );
640                               
641                                if( valueCallback != null ) {
642                                        def name = criterion.entity + ' ' + criterion.field
643       
644                                        results.each { result ->
645                                                saveResultField( result.id, name, valueCallback( result, criterion ) );
646                                        }
647                                }
648                        }
649                }
650        }
651
652        /**
653         * Saves data about template entities to use later on. This data is copied to a special
654         * structure to make it compatible with data fetched from other modules.
655         * @param entities                      List of template entities to find data in
656         * @param criteria                      Criteria to search for
657         * @param valueCallback         Callback to retrieve a specific field from the entity
658         * @see #saveResultField()
659         */
660        protected void saveResultFields( entities, criteria, valueCallback ) {
661                for( criterion in criteria ) {
662                        for( entity in entities ) {
663                                if( criterion.field )
664                                        saveResultField( entity.id, criterion.entity + ' ' + criterion.field, valueCallback( entity, criterion ) )
665                        }
666                }
667        }
668
669
670        /**
671         * Saves a specific field of an object to use later on. Especially useful when looking up data from other modules.
672         * @param id            ID of the object
673         * @param fieldName     Field name that has been searched
674         * @param value         Value of the field
675         */
676        protected void saveResultField( id, fieldName, value ) {
677                if( resultFields[ id ] == null )
678                        resultFields[ id ] = [:]
679
680                // Handle special cases
681                if( value == null )
682                        value = "";
683
684                if( value instanceof Collection ) {
685                        value = value.findAll { it != null }
686                }
687
688                resultFields[ id ][ fieldName ] = value;
689        }
690
691        /**
692         * Removes all data from the result field map
693         */
694        protected void clearResultFields() {
695                resultFields = [:]
696        }
697
698        /**
699         * 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.
700         *
701         * 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
702         * query.)
703         * @return      Map with the entity id as a key, and a field-value map as value
704         */
705        public Map getShowableResultFields() {
706                def resultIds = getResults()*.id;
707                return getResultFields().findAll {
708                        resultIds.contains( it.key )
709                }
710        }
711       
712        /**
713         * Returns the field names that are found in the map with showable result fields
714         *
715         * @param fields        Map with showable result fields
716         * @see getShowableResultFields
717         * @return
718         */
719        public List getShowableResultFieldNames( fields ) {
720                return fields.values()*.keySet().flatten().unique();
721        }
722
723       
724        /************************************************************************
725         *
726         * Getters and setters
727         *
728         ************************************************************************/
729       
730        /**
731        * Returns a list of Criteria
732        */
733   public List getCriteria() { return criteria; }
734
735   /**
736        * Sets a new list of criteria
737        * @param c      List with criteria objects
738        */
739   public void setCriteria( List c ) { criteria = c; }
740
741   /**
742        * Adds a criterion to this query
743        * @param c      Criterion
744        */
745   public void addCriterion( Criterion c ) {
746           if( criteria )
747                   criteria << c;
748           else
749                   criteria = [c];
750   }
751
752   /**
753        * Retrieves the results found using this query. The result is empty is
754        * the query has not been executed yet.
755        */
756   public List getResults() { return results; }
757
758   /**
759        * Returns the results found using this query, filtered by a list of ids.
760        * @param selectedIds    List with ids of the entities you want to return.
761        * @return       A list with only those results for which the id is in the selectedIds
762        */
763   public List filterResults( List selectedIds ) {
764           if( !selectedIds || !results )
765                   return results
766
767           return results.findAll {
768                   selectedIds.contains( it.id )
769           }
770   }
771
772   /**
773        * Returns a list of fields for the results of this query. The fields returned are those
774        * fields that the query searched for.
775        */
776   public Map getResultFields() { return resultFields; }
777       
778        public String toString() {
779                return ( this.entity ? this.entity + " search" : "Search" ) + " " + this.id
780        }
781       
782        public boolean equals( Object o ) {
783                if( o == null )
784                        return false
785               
786                if( !( o instanceof Search ) ) 
787                        return false
788                       
789                Search s = (Search) o;
790               
791                return (        searchMode              == s.searchMode && 
792                                        entity                  == s.entity && 
793                                        criteria.size() == s.criteria.size() && 
794                                        s.criteria.containsAll( criteria ) && 
795                                        criteria.containsAll( s.criteria ) );
796        }
797}
Note: See TracBrowser for help on using the repository browser.