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

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

Improvements in querying in order to be able to refine searches in a module.

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