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

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

Updated assay export to be able to export multiple assays or studies (in the search results page).
Also changed the assay export such that the assay can still be exported if the module is not reachable (without module measurements but with a message in the excel sheet)

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