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

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

Implemented searching in 'all fields'

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