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

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