source: trunk/grails-app/controllers/dbnp/query/AdvancedQueryController.groovy @ 1717

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

Implemented searching for 'any field' in advanced query

  • Property svn:keywords set to Rev Author Date
File size: 17.5 KB
Line 
1package dbnp.query
2
3import dbnp.modules.*
4import org.dbnp.gdt.*
5
6// TODO: Make use of the searchable-plugin or Lucene possibilities instead of querying the database directly
7
8/**
9 * Basic web interface for searching within studies
10 *
11 * @author Robert Horlings (robert@isdat.nl)
12 */
13class AdvancedQueryController {
14        def moduleCommunicationService;
15        def authenticationService
16
17        def entitiesToSearchFor = [ 'Study': 'Studies', 'Sample': 'Samples', 'Assay': 'Assays']
18
19        /**
20         * Shows search screen
21         */
22        def index = {
23                // Check whether criteria have been given before
24                def criteria = [];
25                if( params.criteria ) {
26                        criteria = parseCriteria( params.criteria, false )
27                }
28                [searchModes: SearchMode.values(), entitiesToSearchFor: entitiesToSearchFor, searchableFields: getSearchableFields(), criteria: criteria]
29        }
30
31        /**
32         * Searches for studies or samples based on the user parameters.
33         *
34         * @param       entity          The entity to search for ( 'Study' or 'Sample' )
35         * @param       criteria        HashMap with the values being hashmaps with field, operator and value.
36         *                                              [ 0: [ field: 'Study.name', operator: 'equals', value: 'term' ], 1: [..], .. ]
37         */
38        def search = {
39                if( !params.criteria ) {
40                        flash.error = "No criteria given to search for. Please try again.";
41                        redirect( action: 'index' )
42                }
43
44                if( !params.entity || !entitiesToSearchFor*.key.contains( params.entity ) ) {
45                        flash.error = "No or incorrect entity given to search for. Please try again.";
46                        redirect( action: 'index', params: [ criteria: parseCriteria( params.criteria ) ] )
47                }
48
49                // Create a search object and let it do the searching
50                Search search = determineSearch( params.entity );
51                String view = determineView( params.entity );
52
53                // Choose between AND and OR search. Default is given by the Search class itself.
54                switch( params.operator?.toString()?.toLowerCase() ) {
55                        case "or":
56                                search.searchMode = SearchMode.or;
57                                break;
58                        case "and":
59                                search.searchMode = SearchMode.and;
60                                break;
61                }
62
63                search.execute( parseCriteria( params.criteria ) );
64
65                // Save search in session
66                def queryId = saveSearch( search );
67                render( view: view, model: [search: search, queryId: queryId, actions: determineActions(search)] );
68        }
69
70        /**
71         * Removes a specified search from session
72         * @param       id      queryId of the search to discard
73         */
74        def discard = {
75                def queryIds = params.list( 'id' );
76                queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) }
77
78                if( queryIds.size() == 0 ) {
79                        flash.error = "Incorrect search ID given to discard"
80                        redirect( action: "index" );
81                        return
82                }
83
84                queryIds.each { queryId ->
85                        discardSearch( queryId );
86                }
87
88                if( queryIds.size() > 1 ) {
89                        flash.message = "Searches have been discarded"
90                } else {
91                        flash.message = "Search has been discarded"
92                }
93                redirect( action: "list" );
94        }
95
96        /**
97         * Shows a specified search from session
98         * @param       id      queryId of the search to show
99         */
100        def show = {
101                def queryId = params.int( 'id' );
102
103                if( !queryId ) {
104                        flash.error = "Incorrect search ID given to show"
105                        redirect( action: "index" );
106                        return
107                }
108
109                // Retrieve the search from session
110                Search s = retrieveSearch( queryId );
111                if( !s ) {
112                        flash.message = "Specified search could not be found"
113                        redirect( action: "index" );
114                        return;
115                }
116               
117                // Attach all objects to the current hibernate thread, because the
118                // object might be attached to an old thread, since the results are
119                // saved in session
120                s.getResults().each {
121                        it.attach();
122                }
123
124                // Determine which view to show
125                def view = determineView( s.entity );
126                render( view: view, model: [search: s, queryId: queryId, actions: determineActions(s)] );
127        }
128
129        /**
130         * Performs an action on specific searchResults
131         * @param       queryId         queryId of the search to show
132         * @param       id                      list with the ids of the results to perform the action on
133         * @param       actionName      Name of the action to perform
134         */
135        def performAction = {
136                def queryId = params.int( 'queryId' );
137                def selectedIds = params.list( 'id' ).findAll { it.isLong() }.collect { Long.parseLong(it) }
138                def actionName = params.actionName;
139                def moduleName = params.moduleName;
140
141                if( !queryId ) {
142                        flash.error = "Incorrect search ID given to show"
143                        redirect( action: "index" );
144                        return
145                }
146               
147                // Retrieve the search from session
148                Search s = retrieveSearch( queryId );
149                if( !s ) {
150                        flash.message = "Specified search could not be found"
151                        redirect( action: "list" );
152                        return;
153                }
154
155                // Determine the possible actions and build correct urls
156                def actions = determineActions(s, selectedIds );
157
158                // Find the right action to perform
159                def redirectUrl;
160                for( action in actions ) {
161                        if( action.module == moduleName && action.name == actionName ) {
162                                redirectUrl = action.url;
163                                break;
164                        }
165                }
166               
167                if( !redirectUrl ) {
168                        flash.error = "No valid action is given to perform";
169                        redirect( action: "show", id: queryId );
170                        return;
171                }
172               
173                redirect( url: redirectUrl );
174        }
175       
176        /**
177         * Shows a list of searches that have been saved in session
178         * @param       id      queryId of the search to show
179         */
180        def list = {
181                def searches = listSearches();
182
183                if( !searches || searches.size() == 0 ) {
184                        flash.message = "No previous searches found";
185                        redirect( action: "index" );
186                        return;
187                }
188                [searches: searches]
189        }
190
191        /**
192         * Shows a search screen where the user can search within the results of another search
193         * @param       id      queryId of the search to search in
194         */
195        def searchIn = {
196                def queryIds = params.list( 'id' );
197                queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) }
198
199                if( queryIds.size() == 0 ) {
200                        flash.error = "Incorrect search ID given to search in"
201                        redirect( action: "list" );
202                        return
203                }
204
205                // Retrieve the searches from session
206                def params = [:]
207                queryIds.eachWithIndex { queryId, idx ->
208                        Search s = retrieveSearch( queryId );
209                        if( !s ) {
210                                flash.message = "Specified search " + queryId + " could not be found"
211                                return;
212                        } else {
213                                params[ "criteria." + idx + ".entityfield" ] = s.entity;
214                                params[ "criteria." + idx + ".operator" ] = "in";
215                                params[ "criteria." + idx + ".value" ] = queryId;
216                        }
217                }
218
219                redirect( action: "index", params: params)
220        }
221
222        /**
223         * Combines the results of multiple searches
224         * @param       id      queryIds of the searches to combine
225         */
226        def combine = {
227                def queryIds = params.list( 'id' );
228                queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) }
229
230                if( queryIds.size() == 0 ) {
231                        flash.error = "Incorrect search ID given to combine"
232                        redirect( action: "index" );
233                        return
234                }
235
236                // First determine whether the types match
237                def searches = [];
238                def type = "";
239                flash.error = "";
240                queryIds.eachWithIndex { queryId, idx ->
241                        Search s = retrieveSearch( queryId );
242                        if( !s ) {
243                                return;
244                        }
245
246                        if( type ) {
247                                if( type != s.entity ) {
248                                        flash.error = type + " and " + s.entity.toLowerCase() + " queries can't be combined. Selected queries of one type.";
249                                        return
250                                }
251                        } else {
252                                type = s.entity
253                        }
254                }
255
256                if( flash.error ) {
257                        redirect( action: "list" );
258                        return;
259                }
260
261                if( !type ) {
262                        flash.error = "No correct query ids were given."
263                        redirect( action: "list" );
264                        return;
265                }
266
267                // Retrieve the searches from session
268                Search combined = determineSearch( type );
269                combined.searchMode = SearchMode.or;
270
271                queryIds.eachWithIndex { queryId, idx ->
272                        Search s = retrieveSearch( queryId );
273                        if( s ) {
274                                combined.addCriterion( new Criterion( entity: type, field: null, operator: Operator.insearch, value: s ) );
275                        }
276                }
277
278                // Execute search to combine the results
279                combined.execute();
280
281                def queryId = saveSearch( combined );
282                redirect( action: "show", id: queryId );
283        }
284
285        protected String determineView( String entity ) {
286                switch( entity ) {
287                        case "Study":   return "studyresults";  break;
288                        case "Sample":  return "sampleresults"; break;
289                        case "Assay":   return "assayresults";  break;
290                        default:                return "results"; break;
291                }
292        }
293
294        /**
295         * Returns the search object used for searching
296         */
297        protected Search determineSearch( String entity ) {
298                switch( entity ) {
299                        case "Study":   return new StudySearch();
300                        case "Sample":  return new SampleSearch();
301                        case "Assay":   return new AssaySearch();
302                       
303                        // This exception will only be thrown if the entitiesToSearchFor contains more entities than
304                        // mentioned in this switch structure.
305                        default:                throw new Exception( "Can't search for entities of type " + entity );
306                }
307        }
308
309        /**
310         * Returns a map of entities with the names of the fields the user can search on
311         * @return
312         */
313        protected def getSearchableFields() {
314                def fields = [:];
315
316                // Retrieve all local search fields
317                getEntities().each {
318                        def entity = getEntity( 'dbnp.studycapturing.' + it );
319
320                        if( entity ) {
321                                def domainFields = entity.giveDomainFields();
322                                def templateFields = TemplateField.findAllByEntity( entity )
323
324                                def fieldNames = ( domainFields + templateFields ).collect { it.name }.unique() + 'Template' + '*'
325
326                                fields[ it ] = fieldNames.sort { a, b ->
327                                        def aUC = a.size() > 1 ? a[0].toUpperCase() + a[1..-1] : a;
328                                        def bUC = b.size() > 1 ? b[0].toUpperCase() + b[1..-1] : b;
329                                        aUC <=> bUC
330                                };
331                        }
332                }
333
334                // Loop through all modules and check which fields are searchable
335                // Right now, we just combine the results for different entities
336                AssayModule.list().each { module ->
337                        def callUrl = module.url + '/rest/getQueryableFields'
338                        try {
339                                def json = moduleCommunicationService.callModuleMethod( module.url, callUrl );
340                                def moduleFields = [];
341                                entitiesToSearchFor.each { entity ->
342                                        if( json[ entity.key ] ) {
343                                                json[ entity.key ].each { field ->
344                                                        moduleFields << field.toString();
345                                                }
346                                        }
347                                }
348
349                                // Remove 'module' from module name
350                                def moduleName = module.name.replace( 'module', '' ).trim()
351
352                                fields[ moduleName ] = moduleFields.unique() + '*';
353                        } catch( Exception e ) {
354                                log.error( "Error while retrieving queryable fields from " + module.name + ": " + e.getMessage() )
355                        }
356                }
357
358                return fields;
359        }
360
361        /**
362         * Parses the criteria from the query form given by the user
363         * @param       c       Data from the input form and had a form like
364         *
365         *      [
366         *              0: [entityfield:a.b, operator: b, value: c],
367         *              0.entityfield: a.b,
368         *              0.operator: b,
369         *              0.field: c
370         *              1: [entityfield:f.q, operator: e, value: d],
371         *              1.entityfield: f.q,
372         *              1.operator: e,
373         *              1.field: d
374         *      ]
375         * @param parseSearchIds        Determines whether searches are returned instead of their ids
376         * @return                                      List with Criterion objects
377         */
378        protected List parseCriteria( def formCriteria, def parseSearchIds = true ) {
379                ArrayList list = [];
380                flash.error = "";
381                // Loop through all keys of c and remove the non-numeric ones
382                for( c in formCriteria ) {
383                        if( c.key ==~ /[0-9]+/ && c.value.entityfield ) {
384                                def formCriterion = c.value;
385
386                                Criterion criterion = new Criterion();
387
388                                // Split entity and field
389                                def field = formCriterion.entityfield?.split( /\./ );
390                                if( field.size() > 1 ) {
391                                        criterion.entity = field[0].toString();
392                                        criterion.field = field[1].toString();
393                                } else {
394                                        criterion.entity = field[0];
395                                        criterion.field = null;
396                                }
397
398                                // Convert operator string to Operator-enum field
399                                try {
400                                        criterion.operator = Criterion.parseOperator( formCriterion.operator );
401                                } catch( Exception e) {
402                                        log.debug "Operator " + formCriterion.operator + " could not be parsed: " + e.getMessage();
403                                        flash.error += "Criterion could not be used: operator " + formCriterion.operator + " is not valid.<br />\n";
404                                        continue;
405                                }
406
407                                // Special case of the 'in' operator
408                                if( criterion.operator == Operator.insearch ) {
409                                        Search s
410                                        try {
411                                                s = retrieveSearch( Integer.parseInt( formCriterion.value ) );
412                                        } catch( Exception e ) {}
413
414                                        if( !s ) {
415                                                flash.error += "Can't search within previous query: query not found";
416                                                continue;
417                                        }
418
419                                        if( parseSearchIds ) {
420                                                criterion.value = s
421                                        } else {
422                                                criterion.value = s.id
423                                        }
424                                } else {
425                                        // Copy value
426                                        criterion.value = formCriterion.value;
427                                }
428
429                                list << criterion;
430                        }
431                }
432
433                return list;
434        }
435
436        /**
437         * Returns all entities for which criteria can be entered
438         * @return
439         */
440        protected def getEntities() {
441                return [ 'Study', 'Subject', 'Sample', 'Event', 'SamplingEvent', 'Assay' ]
442        }
443
444        /**
445         * Creates an object of the given entity.
446         *
447         * @return False if the entity is not a subclass of TemplateEntity
448         */
449        protected def getEntity( entityName ) {
450                // Find the templates
451                def entity
452                try {
453                        entity = Class.forName(entityName, true, this.getClass().getClassLoader())
454
455                        // succes, is entity an instance of TemplateEntity?
456                        if (entity.superclass =~ /TemplateEntity$/ || entity.superclass.superclass =~ /TemplateEntity$/) {
457                                return entity;
458                        } else {
459                                return false;
460                        }
461                } catch( ClassNotFoundException e ) {
462                        log.error "Class " + entityName + " not found: " + e.getMessage()
463                        return null;
464                }
465
466        }
467
468
469        /***************************************************************************
470         *
471         * Methods for saving results in session
472         *
473         ***************************************************************************/
474
475        /**
476         * Saves the given search in session. Any search with the same criteria will be overwritten
477         * 
478         * @param s             Search to save
479         * @return              Id of the search for later reference
480         */
481        protected int saveSearch( Search s ) {
482                if( !session.queries )
483                        session.queries = [:]
484
485                // First check whether a search with the same criteria is already present
486                def previousSearch = retrieveSearch( s );
487
488                def id
489                if( previousSearch ) {
490                        id = previousSearch.id;
491                } else {
492                        // Determine unique id
493                        id = ( session.queries*.key.max() ?: 0 ) + 1;
494                }
495
496                s.id = id;
497                session.queries[ id ] = s;
498
499                return id;
500        }
501
502        /**
503         * Retrieves a search from session with the same criteria as given
504         * @param s                     Search that is used as an example to search for
505         * @return                      Search that has this criteria, or null if no such search is found.
506         */
507        protected Search retrieveSearch( Search s ) {
508                if( !session.queries )
509                        return null
510
511                for( query in session.queries ) {
512                        def value = query.value;
513
514                        if( s.equals( value ) )
515                                return value
516                }
517
518                return null;
519        }
520
521
522        /**
523         * Retrieves a search from session
524         * @param id    Id of the search
525         * @return              Search that belongs to this ID or null if no search is found
526         */
527        protected Search retrieveSearch( int id ) {
528                if( !session.queries || !session.queries[ id ] )
529                        return null
530
531                if( !( session.queries[ id ] instanceof Search ) )
532                        return null;
533
534                return (Search) session.queries[ id ]
535        }
536
537        /**
538         * Removes a search from session
539         * @param id    Id of the search
540         * @return      Search that belonged to this ID or null if no search is found
541         */
542        protected Search discardSearch( int id ) {
543                if( !session.queries || !session.queries[ id ] )
544                        return null
545
546                def sessionSearch = session.queries[ id ];
547
548                session.queries.remove( id );
549
550                if( !( sessionSearch instanceof Search ) )
551                        return null;
552
553                return (Search) sessionSearch
554        }
555
556        /**
557         * Retrieves a list of searches from session
558         * @return      List of searches from session
559         */
560        protected List listSearches() {
561                if( !session.queries )
562                        return []
563
564                return session.queries*.value.toList()
565        }
566
567        /**
568         * Determine a list of actions that can be performed on specific entities
569         * @param entity                Name of the entity that the actions could be performed on
570         * @param selectedIds   List with ids of the selected items to perform an action on
571         * @return
572         */
573        protected List determineActions( Search s, def selectedIds = null ) {
574                return gscfActions( s, selectedIds ) + moduleActions( s, selectedIds );
575        }
576
577        /**
578         * Determine a list of actions that can be performed on specific entities by GSCF
579         * @param entity        Name of the entity that the actions could be performed on
580         * @param selectedIds   List with ids of the selected items to perform an action on
581         */
582        protected List gscfActions(Search s, def selectedIds = null) {
583                switch(s.entity) {
584                        case "Study":
585                                def ids = []
586                                s.filterResults(selectedIds).each {
587                                        ids << it.id
588                                }
589                               
590                                return [[
591                                                module: "gscf",
592                                                name:"simpletox",
593                                                description: "Export as SimpleTox",
594                                                url: createLink( controller: "exporter", action: "export", params: [ 'ids' : ids ] )
595                                        ]]
596                        case "Sample":
597                                return []
598                        default:
599                                return [];
600                }
601        }
602
603        /**
604         * Determine a list of actions that can be performed on specific entities by other modules
605         * @param entity        Name of the entity that the actions could be performed on
606         */
607        protected List moduleActions(Search s, def selectedIds = null) {
608                def actions = []
609
610                if( !s.getResults() || s.getResults().size() == 0 )
611                        return []
612
613                // Loop through all modules and check which actions can be performed on the
614                AssayModule.list().each { module ->
615                        // Remove 'module' from module name
616                        def moduleName = module.name.replace( 'module', '' ).trim()
617                        try {
618                                def callUrl = module.url + "/rest/getPossibleActions?entity=" + s.entity
619                                def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl );
620
621                                // Check whether the entity is present in the return value
622                                if( json[ s.entity ] ) {
623                                        json[ s.entity ].each { action ->
624                                                def url = action.url ?: module.url + "/action/" + action.name
625                                               
626                                                if( url.find( /\?/ ) )
627                                                        url += "&"
628                                                else
629                                                        url += "?"
630                                               
631                                                url += "entity=" + s.entity
632                                                url += "&" + s.filterResults(selectedIds).collect { "tokens=" + it.giveUUID() }.join( "&" )
633                                                actions << [
634                                                                        module: moduleName,
635                                                                        name: action.name,
636                                                                        description: action.description + " (" + moduleName + ")",
637                                                                        url: url
638                                                                ];
639                                        }
640                                }
641                        } catch( Exception e ) {
642                                // Exception is thrown when the call to the module fails. No problems though.
643                                log.error "Error while fetching possible actions from " + module.name + ": " + e.getMessage()
644                        }
645                }
646
647                return actions;
648        }
649
650}
Note: See TracBrowser for help on using the repository browser.