source: trunk/grails-app/controllers/nl/tno/massSequencing/query/QueryController.groovy @ 66

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

Improved searches

File size: 17.6 KB
Line 
1package nl.tno.massSequencing.query
2
3import java.util.List
4
5import nl.tno.massSequencing.classification.*
6import nl.tno.massSequencing.*
7import nl.tno.massSequencing.integration.*
8
9class QueryController {
10        def gscfService
11       
12        /**
13         * Shows a search page for classifications
14         */
15        def index = {
16                // Check whether criteria have been given before
17                def criteria = [];
18                println "Params: " + params.criteria
19                if( params.criteria ) {
20                        criteria = parseCriteria( params.criteria, false )
21                }
22               
23                // Determine level names
24                def (maxLevel, minLevel) = Classification.determineMinAndMaxLevels();
25                def levels = Taxon.retrieveLevelNames( minLevel, maxLevel );
26
27                // Determine which fields to search in
28                def searchableFields = Taxon.list( sort: "name", order: "asc" ).collect {
29                        return [
30                                'name': it.name,
31                                'level': it.level,
32                                'id': it.id,
33                                'levelName': levels[ it.level ]
34                        ]
35                }
36
37                [searchableFields: searchableFields, criteria: criteria, previousSearches: session.queries, searchModes: SearchMode.values() ]
38        }
39
40        /**
41         * Searches for samples based on the user parameters.
42         *
43         * @param       entity          The entity to search for ( 'Study' or 'Sample' )
44         * @param       criteria        HashMap with the values being hashmaps with field, operator and value.
45         *                                              [ 0: [ field: 'Study.name', operator: 'equals', value: 'term' ], 1: [..], .. ]
46         */
47        def search = {
48                if( !params.criteria ) {
49                        flash.error = "No criteria given to search for. Please try again.";
50                        redirect( action: 'index' )
51                        return;
52                }
53
54                if( !params.entity || params.entity != "AssaySample" ) {
55                        // Pick only correct parameters
56                        def parameters = [:];
57                        params.criteria.each { param ->
58                                if( param.key =~ /^[0-9]$/ ) {
59                                        [ "entity", "taxon", "operator", "value", "factor" ].each { 
60                                                parameters[ "criteria." + param.key + "." + it ] = param.value?.getAt( it );
61                                        }
62                                }
63                        }
64                       
65                        flash.error = "No or incorrect entity given to search for. Please try again.";
66                        redirect( action: 'index', params: parameters )
67                        return;
68                }
69
70                // Create a search object and let it do the searching
71                Search search;
72                String view;
73                try { 
74                        search = determineSearch( params.entity );
75                        view = determineView( params.entity );
76                } catch( Exception e ) {
77                        flash.error = e.message;
78                        redirect( action: 'index' );
79                        return;
80                }
81
82                // Choose between AND and OR search. Default is given by the Search class itself.
83                switch( params.operator?.toString()?.toLowerCase() ) {
84                        case "or":
85                                search.searchMode = SearchMode.or;
86                                break;
87                        case "and":
88                                search.searchMode = SearchMode.and;
89                                break;
90                }
91
92                search.execute( parseCriteria( params.criteria ) );
93
94                // Save search in session
95                def queryId = saveSearch( search );
96                render( view: view, model: [search: search, queryId: queryId, actions: determineActions(search) ] );
97        }
98
99        /**
100         * Shows a list of searches that have been saved in session
101         * @param       id      queryId of the search to show
102         */
103        def list = {
104                def searches = listSearches();
105
106                if( !searches || searches.size() == 0 ) {
107                        flash.message = "No previous searches found";
108                        redirect( action: "index" );
109                        return;
110                }
111
112                [searches: searches]
113        }
114
115        /**
116         * Shows a specified search from session
117         * @param       id      queryId of the search to show
118         */
119        def show = {
120                def queryId = params.int( 'id' );
121
122                if( !queryId ) {
123                        flash.error = "Incorrect search ID given to show"
124                        redirect( action: "index" );
125                        return
126                }
127
128                // Retrieve the search from session
129                Search s = retrieveSearch( queryId );
130                if( !s ) {
131                        flash.message = "Specified search could not be found"
132                        redirect( action: "index" );
133                        return;
134                }
135
136                // Attach all objects to the current hibernate thread, because the
137                // object might be attached to an old thread, since the results are
138                // saved in session
139                s.getResults().each {
140                        it.attach();
141                }
142
143                // Determine which view to show
144                def view = determineView( s.entity );
145                render( view: view, model: [search: s, queryId: queryId, actions: determineActions(s) ] );
146        }
147       
148       
149        /**
150         * Removes a specified search from session
151         * @param       id      queryId of the search to discard
152         */
153        def discard = {
154                def queryIds = params.list( 'id' );
155                queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) }
156
157                if( queryIds.size() == 0 ) {
158                        flash.error = "Incorrect search ID given to discard"
159                        redirect( action: "index" );
160                        return
161                }
162
163                queryIds.each { queryId ->
164                        discardSearch( queryId );
165                }
166
167                if( queryIds.size() > 1 ) {
168                        flash.message = "Searches have been discarded"
169                } else {
170                        flash.message = "Search has been discarded"
171                }
172                redirect( action: "list" );
173        }
174
175       
176        /**
177         * Shows a search screen where the user can search within the results of another search
178         * @param       id      queryId of the search to search in
179         */
180        def searchIn = {
181                def queryIds = params.list( 'id' );
182                queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) }
183
184                if( queryIds.size() == 0 ) {
185                        flash.error = "Incorrect search ID given to search in"
186                        redirect( action: "list" );
187                        return
188                }
189
190                // Retrieve the searches from session
191                def params = [:]
192                queryIds.eachWithIndex { queryId, idx ->
193                        Search s = retrieveSearch( queryId );
194                        if( !s ) {
195                                flash.message = "Specified search " + queryId + " could not be found"
196                                return;
197                        } else {
198                                params[ "criteria." + idx + ".entity" ] = s.entity;
199                                params[ "criteria." + idx + ".operator" ] = "in";
200                                params[ "criteria." + idx + ".value" ] = queryId;
201                        }
202                }
203
204                redirect( action: "index", params: params )
205        }
206
207        /**
208         * Combines the results of multiple searches
209         * @param       id      queryIds of the searches to combine
210         */
211        def combine = {
212                def queryIds = params.list( 'id' );
213                queryIds = queryIds.findAll { it.isInteger() }.collect { Integer.valueOf( it ) }
214
215                if( queryIds.size() == 0 ) {
216                        flash.error = "Incorrect search ID given to combine"
217                        redirect( action: "index" );
218                        return
219                }
220
221                // First determine whether the types match
222                def searches = [];
223                def type = "";
224                flash.error = "";
225                queryIds.eachWithIndex { queryId, idx ->
226                        Search s = retrieveSearch( queryId );
227                        if( !s ) {
228                                return;
229                        }
230
231                        if( type ) {
232                                if( type != s.entity ) {
233                                        flash.error = type + " and " + s.entity.toLowerCase() + " queries can't be combined. Selected queries of one type.";
234                                        return
235                                }
236                        } else {
237                                type = s.entity
238                        }
239                }
240
241                if( flash.error ) {
242                        redirect( action: "list" );
243                        return;
244                }
245
246                if( !type ) {
247                        flash.error = "No correct query ids were given."
248                        redirect( action: "list" );
249                        return;
250                }
251
252                // Retrieve the searches from session
253                Search combined = determineSearch( type );
254                combined.searchMode = SearchMode.or;
255
256                queryIds.eachWithIndex { queryId, idx ->
257                        Search s = retrieveSearch( queryId );
258                        if( s ) {
259                                combined.addCriterion( new Criterion( entity: type, taxon: null, operator: Operator.insearch, value: s ) );
260                        }
261                }
262
263                // Execute search to combine the results
264                combined.execute();
265
266                def queryId = saveSearch( combined );
267                redirect( action: "show", id: queryId );
268        }
269       
270        /**
271        * Registers a search from a module with GSCF, in order to be able to refine the searches
272        */
273   def refineExternal = {
274           // Determine parameters and retrieve objects based on the tokens
275           def name = params.name
276           def url = params.url
277           def entity = params.list( "entity" )[ 0 ]; 
278           def tokens = params.list( 'tokens' );
279           def results
280           
281           switch( entity ) {
282                   case "Study":
283                           results = Study.findAll( "from Study s where s.studyToken IN (:tokens)", [ 'tokens': tokens ] )
284                           break;
285                   case "Assay":
286                           results = Assay.findAll( "from Assay a where a.assayToken IN (:tokens)", [ 'tokens': tokens ] )
287                           break; 
288                   case "Sample":
289                           results = Sample.findAll( "from Sample s where s.sampleToken IN (:tokens)", [ 'tokens': tokens ] )
290                           break;
291                   default:
292                           response.sendError( 400 );
293                           log.debug "Entity given: " + entity;
294                           render "The given entity is not supported. Choose one of Study, Assay or Sample"
295                           return;
296           }
297           
298           // Register and save search
299           Search s = Search.register( name, url, entity, results );
300           int searchId = saveSearch( s );
301           
302           println "Params: " + params.url
303           println "Search: " + s.url
304           
305           // Redirect to the search screen
306           def params = [
307                   "criteria.0.entity": s.entity,
308                   "criteria.0.operator": "in",
309                   "criteria.0.value": searchId
310           ];
311
312           redirect( action: "index", params: params)
313   }
314       
315       
316       
317        /**
318         * Parses the criteria from the query form given by the user
319         * @param       c       Data from the input form and had a form like
320         *
321         *      [
322         *              0: [entity:a, operator: b, value: c],
323         *              0.entity: a,
324         *              0.operator: b,
325         *              0.field: c
326         *              1: [taxon:q, operator: e, value: d],
327         *              1.taxon: q,
328         *              1.operator: e,
329         *              1.field: d
330         *      ]
331         * @param parseSearchIds        Determines whether searches are returned instead of their ids
332         * @return                                      List with Criterion objects
333         */
334        protected List parseCriteria( Map formCriteria, def parseSearchIds = true ) {
335                ArrayList list = [];
336                flash.error = "";
337
338                // Loop through all keys of c and remove the non-numeric ones
339                for( c in formCriteria ) {
340                        if( c.key ==~ /[0-9]+/ && ( c.value.taxon || c.value.entity )) {
341                                def formCriterion = c.value;
342
343                                Criterion criterion = new Criterion();
344
345                                // Convert operator string to Operator-enum field
346                                try {
347                                        criterion.operator = Criterion.parseOperator( formCriterion.operator.toString() );
348                                } catch( Exception e) {
349                                        log.debug "Operator " + formCriterion.operator + " could not be parsed: " + e.getMessage();
350                                        flash.error += "Criterion could not be used: operator " + formCriterion.operator.encodeAsHtml() + " is not valid.<br />\n";
351                                        continue;
352                                }
353
354                                // Special case of the 'in' operator
355                                if( criterion.operator == Operator.insearch ) {
356                                       
357                                        Search s
358                                        try {
359                                                s = retrieveSearch( Integer.parseInt( formCriterion.value ) );
360                                        } catch( Exception e ) {}
361
362                                        if( !s ) {
363                                                flash.error += "Can't search within previous query: query not found";
364                                                continue;
365                                        }
366                                       
367                                        // Determine the entity to search in
368                                        criterion.entity = s.entity;
369
370                                        if( parseSearchIds ) {
371                                                criterion.value = s
372                                        } else {
373                                                criterion.value = s.id
374                                        }
375                                } else {
376                                       
377                                        // Determine which taxon to search on
378                                        def field = formCriterion.taxon
379                                        if( field.isLong() ) {
380                                                criterion.taxon = Taxon.get( field.toLong() );
381                                        } else {
382                                                log.debug "Taxon " + formCriterion.taxon + " could not be parsed: " + e.getMessage();
383                                                flash.error += "Criterion could not be used: taxon " + formCriterion.taxon.encodeAsHtml() + " is not valid.<br />\n";
384                                                continue;
385                                        }
386                               
387                                        // Copy value
388                                        if( formCriterion.factor.isDouble() )
389                                                criterion.factor = Double.parseDouble( formCriterion.factor );
390                                       
391                                        // Check if the value is a taxon, percentage or absolute value
392                                        if( formCriterion.value =~ /%$/ ) {
393                                                // Percentage: parse the value, divide it by 100 and store it as double
394                                                def percentage = ( formCriterion.value =~ /[^0-9\.]/ ).replaceAll( "" );
395                                               
396                                                if( !percentage.isDouble() ) {
397                                                        log.debug "Criterion value contains % sign but could not be parsed as a percentage";
398                                                        flash.error += "Criterion could not be used: value " + formCriterion.value.encodeAsHtml() + " could not be parsed as a percentage.<br />\n";
399                                                        continue;
400                                                }
401                                               
402                                                criterion.value = Double.valueOf( percentage ) / 100.0;
403                                        } else if( formCriterion.othertaxon =~ /^taxon:[0-9]+$/ ) {
404                                                // Taxon: parse the taxon ID and set taxon
405                                                def otherTaxonId = ( formCriterion.othertaxon =~ /[^0-9]/ ).replaceAll( "" );
406                                                def otherTaxon = Taxon.get( Long.valueOf( otherTaxonId ) );
407                                               
408                                                if( !otherTaxon ) {
409                                                        log.debug "Criterion value looks like a taxon, but taxon can't be found for ID: " + otherTaxonId + " (value: " + formCriterion.othertaxon + ")";
410                                                        flash.error += "Criterion could not be used: value " + formCriterion.othertaxon.encodeAsHtml() + " could not be parsed as a taxon.<br />\n";
411                                                        continue;
412                                                }
413                                               
414                                                criterion.value = otherTaxon;
415                                        } else if( formCriterion.value =~ /[0-9]+/ ) {
416                                                // A number is present in the string. Parse it, and take it as the absolute value
417                                                def number = ( formCriterion.value =~ /[^0-9]/ ).replaceAll( "" );
418                                               
419                                                if( !number.isLong() ) {
420                                                        log.debug "Criterion value looks like a number, but can't be parsed: " + number + " (value: " + formCriterion.value + ")";
421                                                        flash.error += "Criterion could not be used: value " + formCriterion.value.encodeAsHtml() + " could not be parsed as an absolute value.<br />\n";
422                                                        continue;
423                                                }
424                                               
425                                                criterion.value = Long.parseLong( number );
426                                        } else {
427                                                log.debug "Criterion value can't be parsed at all: " + formCriterion.value;
428                                                flash.error += "Criterion could not be used: value " + formCriterion.value.encodeAsHtml() + " could not be parsed.<br />\n";
429                                                continue;
430                                        }
431                                }
432
433                                list << criterion;
434                        }
435                }
436
437                return list;
438        }
439
440        protected String determineView( String entity ) {
441                return "results";
442        }
443
444        /**
445         * Returns the search object used for searching
446         */
447        protected Search determineSearch( String entity ) {
448                switch( entity ) {
449                        case "AssaySample":     return new AssaySampleSearch( session.user );
450
451                        // This exception will only be thrown if the entitiesToSearchFor contains more entities than
452                        // mentioned in this switch structure.
453                        default:                throw new Exception( "Can't search for entities of type " + entity );
454                }
455        }
456
457        /***************************************************************************
458         *
459         * Methods for saving results in session
460         *
461         ***************************************************************************/
462
463        /**
464         * Saves the given search in session. Any search with the same criteria will be overwritten
465         *
466         * @param s             Search to save
467         * @return              Id of the search for later reference
468         */
469        protected int saveSearch( Search s ) {
470                if( !session.queries )
471                        session.queries = [:]
472
473                // First check whether a search with the same criteria is already present
474                def previousSearch = retrieveSearch( s );
475
476                def id
477                if( previousSearch ) {
478                        id = previousSearch.id;
479                } else {
480                        // Determine unique id
481                        id = ( session.queries*.key.max() ?: 0 ) + 1;
482                }
483
484                s.id = id;
485
486                if( !s.url )
487                        s.url = g.createLink( controller: "query", action: "show", id: id, absolute: true );
488
489                session.queries[ id ] = s;
490
491                return id;
492        }
493
494        /**
495         * Retrieves a search from session with the same criteria as given
496         * @param s                     Search that is used as an example to search for
497         * @return                      Search that has this criteria, or null if no such search is found.
498         */
499        protected Search retrieveSearch( Search s ) {
500                if( !session.queries )
501                        return null
502
503                for( query in session.queries ) {
504                        def value = query.value;
505
506                        if( s.equals( value ) )
507                                return value
508                }
509
510                return null;
511        }
512
513
514        /**
515         * Retrieves a search from session
516         * @param id    Id of the search
517         * @return              Search that belongs to this ID or null if no search is found
518         */
519        protected Search retrieveSearch( int id ) {
520                if( !session.queries || !session.queries[ id ] )
521                        return null
522
523                if( !( session.queries[ id ] instanceof Search ) )
524                        return null;
525
526                return (Search) session.queries[ id ]
527        }
528
529        /**
530         * Removes a search from session
531         * @param id    Id of the search
532         * @return      Search that belonged to this ID or null if no search is found
533         */
534        protected Search discardSearch( int id ) {
535                if( !session.queries || !session.queries[ id ] )
536                        return null
537
538                def sessionSearch = session.queries[ id ];
539
540                session.queries.remove( id );
541
542                if( !( sessionSearch instanceof Search ) )
543                        return null;
544
545                return (Search) sessionSearch
546        }
547
548        /**
549         * Retrieves a list of searches from session
550         * @return      List of searches from session
551         */
552        protected List listSearches() {
553                if( !session.queries )
554                        return []
555
556                return session.queries*.value.toList()
557        }
558       
559        /**
560        * Determine a list of actions that can be performed on specific entities by GSCF
561        * @param entity Name of the entity that the actions could be performed on
562        * @param selectedTokens List with tokens (UUID) of the selected items to perform an action on
563        */
564   protected List determineActions(Search s, def selectedTokens = null) {
565           def ids = []
566           def tokens = []
567           s.filterResults(selectedTokens).each {
568                   ids << it.id
569                   tokens << it.token()
570           }
571
572           def tokenParamString = tokens.collect { "tokens=" + it }.join( "&" );
573           def idParamString = ids.collect { "ids=" + it }.join( "&" );
574           return [[
575                           module: "gscf",
576                           name: "refine",
577                           type: "refine",
578                           description: "Refine using GSCF",
579                           url: gscfService.urlRegisterSearch( s ) + "?name=" + s.description.encodeAsURL() + "&url=" + s.url.encodeAsURL() + "&entity=" + s.entity.encodeAsURL() + "&" + tokenParamString,
580                           submitUrl: gscfService.urlRegisterSearch( s ),
581                           paramString: tokenParamString
582                   ], [
583                           module: "massSequencing",
584                           name:"fasta",
585                           type: "export",
586                           description: "Export all information",
587                           url: createLink( controller: "assaySample", action: "exportAsFasta", params: [ 'ids' : ids ] ),
588                           submitUrl: createLink( controller: "assaySample", action: "exportAsFasta" ),
589                           paramString: idParamString
590                   ], [
591                           module: "massSequencing",
592                           name:"excel",
593                           type: "export",
594                           description: "Export metadata",
595                           url: createLink( controller: "assaySample", action: "exportMetaData", params: [ 'ids' : ids ] ),
596                           submitUrl: createLink( controller: "assaySample", action: "exportMetaData" ),
597                           paramString: idParamString
598                   ], [
599                           module: "massSequencing",
600                           name: "classification_export",
601                           type: "export",
602                           description: "Export classification",
603                           url: createLink( controller: "classification", action: "export", params: [ 'ids' : ids ] ),
604                           submitUrl: createLink( controller: "classification", action: "export" ),
605                           paramString: idParamString
606                   ],
607           ]
608   }
609
610}
Note: See TracBrowser for help on using the repository browser.