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

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

Removed debug statements

  • Property svn:keywords set to Rev Author Date
File size: 18.9 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                // Redirect to the search screen
269                def params = [
270                        "criteria.0.entityfield": s.entity,
271                        "criteria.0.operator": "in",
272                        "criteria.0.value": searchId
273                ];
274
275                redirect( action: "index", params: params)
276        }
277
278        protected String determineView( String entity ) {
279                switch( entity ) {
280                        case "Study":   return "studyresults";  break;
281                        case "Sample":  return "sampleresults"; break;
282                        case "Assay":   return "assayresults";  break;
283                        default:                return "results"; break;
284                }
285        }
286
287        /**
288         * Returns the search object used for searching
289         */
290        protected Search determineSearch( String entity ) {
291                switch( entity ) {
292                        case "Study":   return new StudySearch();
293                        case "Sample":  return new SampleSearch();
294                        case "Assay":   return new AssaySearch();
295                       
296                        // This exception will only be thrown if the entitiesToSearchFor contains more entities than
297                        // mentioned in this switch structure.
298                        default:                throw new Exception( "Can't search for entities of type " + entity );
299                }
300        }
301
302        /**
303         * Returns a map of entities with the names of the fields the user can search on
304         * @return
305         */
306        protected def getSearchableFields() {
307                def fields = [ '*' : [ '*' ] ]; // Searches for all fields in all objects
308
309                // Retrieve all local search fields
310                getEntities().each {
311                        def entity = getEntity( 'dbnp.studycapturing.' + it );
312
313                        if( entity ) {
314                                def domainFields = entity.giveDomainFields();
315                                def templateFields = TemplateField.findAllByEntity( entity )
316
317                                def fieldNames = ( domainFields + templateFields ).collect { it.name }.unique() + 'Template' + '*'
318
319                                fields[ it ] = fieldNames.sort { a, b ->
320                                        def aUC = a.size() > 1 ? a[0].toUpperCase() + a[1..-1] : a;
321                                        def bUC = b.size() > 1 ? b[0].toUpperCase() + b[1..-1] : b;
322                                        aUC <=> bUC
323                                };
324                        }
325                }
326
327                // Loop through all modules and check which fields are searchable
328                // Right now, we just combine the results for different entities
329                AssayModule.list().each { module ->
330                        def callUrl = module.url + '/rest/getQueryableFields'
331                        try {
332                                def json = moduleCommunicationService.callModuleMethod( module.url, callUrl );
333                                def moduleFields = [];
334                                entitiesToSearchFor.each { entity ->
335                                        if( json[ entity.key ] ) {
336                                                json[ entity.key ].each { field ->
337                                                        moduleFields << field.toString();
338                                                }
339                                        }
340                                }
341
342                                // Remove 'module' from module name
343                                def moduleName = module.name.replace( 'module', '' ).trim()
344
345                                fields[ moduleName ] = moduleFields.unique() + '*';
346                        } catch( Exception e ) {
347                                log.error( "Error while retrieving queryable fields from " + module.name + ": " + e.getMessage() )
348                        }
349                }
350               
351                return fields;
352        }
353
354        /**
355         * Parses the criteria from the query form given by the user
356         * @param       c       Data from the input form and had a form like
357         *
358         *      [
359         *              0: [entityfield:a.b, operator: b, value: c],
360         *              0.entityfield: a.b,
361         *              0.operator: b,
362         *              0.field: c
363         *              1: [entityfield:f.q, operator: e, value: d],
364         *              1.entityfield: f.q,
365         *              1.operator: e,
366         *              1.field: d
367         *      ]
368         * @param parseSearchIds        Determines whether searches are returned instead of their ids
369         * @return                                      List with Criterion objects
370         */
371        protected List parseCriteria( def formCriteria, def parseSearchIds = true ) {
372                ArrayList list = [];
373                flash.error = "";
374               
375                // Loop through all keys of c and remove the non-numeric ones
376                for( c in formCriteria ) {
377                        if( c.key ==~ /[0-9]+/ && c.value.entityfield ) {
378                                def formCriterion = c.value;
379
380                                Criterion criterion = new Criterion();
381
382                                // Split entity and field
383                                def field = formCriterion.entityfield?.split( /\./ );
384                                if( field.size() > 1 ) {
385                                        criterion.entity = field[0].toString();
386                                        criterion.field = field[1].toString();
387                                } else {
388                                        criterion.entity = field[0];
389                                        criterion.field = null;
390                                }
391
392                                // Convert operator string to Operator-enum field
393                                try {
394                                        criterion.operator = Criterion.parseOperator( formCriterion.operator );
395                                } catch( Exception e) {
396                                        log.debug "Operator " + formCriterion.operator + " could not be parsed: " + e.getMessage();
397                                        flash.error += "Criterion could not be used: operator " + formCriterion.operator + " is not valid.<br />\n";
398                                        continue;
399                                }
400
401                                // Special case of the 'in' operator
402                                if( criterion.operator == Operator.insearch ) {
403                                        Search s
404                                        try {
405                                                s = retrieveSearch( Integer.parseInt( formCriterion.value ) );
406                                        } catch( Exception e ) {}
407
408                                        if( !s ) {
409                                                flash.error += "Can't search within previous query: query not found";
410                                                continue;
411                                        }
412
413                                        if( parseSearchIds ) {
414                                                criterion.value = s
415                                        } else {
416                                                criterion.value = s.id
417                                        }
418                                } else {
419                                        // Copy value
420                                        criterion.value = formCriterion.value;
421                                }
422
423                                list << criterion;
424                        }
425                }
426
427                return list;
428        }
429
430        /**
431         * Returns all entities for which criteria can be entered
432         * @return
433         */
434        protected def getEntities() {
435                return [ 'Study', 'Subject', 'Sample', 'Event', 'SamplingEvent', 'Assay' ]
436        }
437
438        /**
439         * Creates an object of the given entity.
440         *
441         * @return False if the entity is not a subclass of TemplateEntity
442         */
443        protected def getEntity( entityName ) {
444                // Find the templates
445                def entity
446                try {
447                        entity = Class.forName(entityName, true, this.getClass().getClassLoader())
448
449                        // succes, is entity an instance of TemplateEntity?
450                        if (entity.superclass =~ /TemplateEntity$/ || entity.superclass.superclass =~ /TemplateEntity$/) {
451                                return entity;
452                        } else {
453                                return false;
454                        }
455                } catch( ClassNotFoundException e ) {
456                        log.error "Class " + entityName + " not found: " + e.getMessage()
457                        return null;
458                }
459
460        }
461
462
463        /***************************************************************************
464         *
465         * Methods for saving results in session
466         *
467         ***************************************************************************/
468
469        /**
470         * Saves the given search in session. Any search with the same criteria will be overwritten
471         * 
472         * @param s             Search to save
473         * @return              Id of the search for later reference
474         */
475        protected int saveSearch( Search s ) {
476                if( !session.queries )
477                        session.queries = [:]
478
479                // First check whether a search with the same criteria is already present
480                def previousSearch = retrieveSearch( s );
481
482                def id
483                if( previousSearch ) {
484                        id = previousSearch.id;
485                } else {
486                        // Determine unique id
487                        id = ( session.queries*.key.max() ?: 0 ) + 1;
488                }
489
490                s.id = id;
491               
492                if( !s.url )
493                        s.url = g.createLink( controller: "advancedQuery", action: "show", id: id, absolute: true );
494               
495                session.queries[ id ] = s;
496
497                return id;
498        }
499
500        /**
501         * Retrieves a search from session with the same criteria as given
502         * @param s                     Search that is used as an example to search for
503         * @return                      Search that has this criteria, or null if no such search is found.
504         */
505        protected Search retrieveSearch( Search s ) {
506                if( !session.queries )
507                        return null
508
509                for( query in session.queries ) {
510                        def value = query.value;
511
512                        if( s.equals( value ) )
513                                return value
514                }
515
516                return null;
517        }
518
519
520        /**
521         * Retrieves a search from session
522         * @param id    Id of the search
523         * @return              Search that belongs to this ID or null if no search is found
524         */
525        protected Search retrieveSearch( int id ) {
526                if( !session.queries || !session.queries[ id ] )
527                        return null
528
529                if( !( session.queries[ id ] instanceof Search ) )
530                        return null;
531
532                return (Search) session.queries[ id ]
533        }
534
535        /**
536         * Removes a search from session
537         * @param id    Id of the search
538         * @return      Search that belonged to this ID or null if no search is found
539         */
540        protected Search discardSearch( int id ) {
541                if( !session.queries || !session.queries[ id ] )
542                        return null
543
544                def sessionSearch = session.queries[ id ];
545
546                session.queries.remove( id );
547
548                if( !( sessionSearch instanceof Search ) )
549                        return null;
550
551                return (Search) sessionSearch
552        }
553
554        /**
555         * Retrieves a list of searches from session
556         * @return      List of searches from session
557         */
558        protected List listSearches() {
559                if( !session.queries )
560                        return []
561
562                return session.queries*.value.toList()
563        }
564
565        /**
566         * Determine a list of actions that can be performed on specific entities
567         * @param entity                Name of the entity that the actions could be performed on
568         * @param selectedIds   List with ids of the selected items to perform an action on
569         * @return
570         */
571        protected List determineActions( Search s, def selectedIds = null ) {
572                return gscfActions( s, selectedIds ) + moduleActions( s, selectedIds );
573        }
574
575        /**
576         * Determine a list of actions that can be performed on specific entities by GSCF
577         * @param entity        Name of the entity that the actions could be performed on
578         * @param selectedTokens        List with tokens (UUID) of the selected items to perform an action on
579         */
580        protected List gscfActions(Search s, def selectedTokens = null) {
581                switch(s.entity) {
582                        case "Study":
583                                def ids = []
584                                s.filterResults(selectedTokens).each {
585                                        ids << it.id
586                                }
587
588                                def paramString = ids.collect { return 'ids=' + it }.join( '&' );
589                               
590                                return [[
591                                                module: "gscf",
592                                                name:"simpletox",
593                                                type: "export",
594                                                description: "Export as SimpleTox",
595                                                url: createLink( controller: "exporter", action: "export", params: [ 'format': 'list', 'ids' : ids ] ),
596                                                submitUrl: createLink( controller: "exporter", action: "export", params: [ 'format': 'list' ] ),
597                                                paramString: paramString
598                                        ], [
599                                                module: "gscf",
600                                                name:"excel",
601                                                type: "export",
602                                                description: "Export as CSV",
603                                                url: createLink( controller: "study", action: "exportToExcel", params: [ 'format': 'list', 'ids' : ids ] ),
604                                                submitUrl: createLink( controller: "study", action: "exportToExcel", params: [ 'format': 'list' ] ),
605                                                paramString: paramString
606                                        ]]
607                        case "Assay":
608                                def ids = []
609                                s.filterResults(selectedTokens).each {
610                                        ids << it.id
611                                }
612
613                                def paramString = ids.collect { return 'ids=' + it }.join( '&' );
614                               
615                                return [[
616                                                module: "gscf",
617                                                name:"excel",
618                                                type: "export",
619                                                description: "Export as CSV",
620                                                url: createLink( controller: "assay", action: "exportToExcel", params: [ 'format': 'list', 'ids' : ids ] ),
621                                                submitUrl: createLink( controller: "assay", action: "exportToExcel", params: [ 'format': 'list' ] ),
622                                                paramString: paramString
623                                        ]]
624                        case "Sample":
625                                return []
626                        default:
627                                return [];
628                }
629        }
630
631        /**
632         * Determine a list of actions that can be performed on specific entities by other modules
633         * @param entity        Name of the entity that the actions could be performed on
634         */
635        protected List moduleActions(Search s, def selectedTokens = null) {
636                def actions = []
637
638                if( !s.getResults() || s.getResults().size() == 0 )
639                        return []
640
641                // Loop through all modules and check which actions can be performed on the
642                AssayModule.list().each { module ->
643                        // Remove 'module' from module name
644                        def moduleName = module.name.replace( 'module', '' ).trim()
645                        try {
646                                def callUrl = module.url + "/rest/getPossibleActions?entity=" + s.entity
647                                def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl );
648
649                                // Check whether the entity is present in the return value
650                                if( json[ s.entity ] ) {
651                                        json[ s.entity ].each { action ->
652                                                def baseUrl = action.url ?: module.url + "/action/" + action.name
653                                                def paramString = s.filterResults(selectedTokens).collect { "tokens=" + it.giveUUID() }.join( "&" )
654                                               
655                                                def url = baseUrl;
656
657                                                if( url.find( /\?/ ) )
658                                                        url += "&"
659                                                else
660                                                        url += "?"
661                                               
662                                                paramString += "&entity=" + s.entity
663                                               
664                                                actions << [
665                                                                        module: moduleName,
666                                                                        name: action.name,
667                                                                        type: action.type ?: 'default',
668                                                                        description: action.description + " (" + moduleName + ")",
669                                                                        url: url + "&" + paramString,
670                                                                        submitUrl: baseUrl,
671                                                                        paramString: paramString
672                                                                ];
673                                        }
674                                }
675                        } catch( Exception e ) {
676                                // Exception is thrown when the call to the module fails. No problems though.
677                                log.error "Error while fetching possible actions from " + module.name + ": " + e.getMessage()
678                        }
679                }
680
681                return actions;
682        }
683
684}
Note: See TracBrowser for help on using the repository browser.