source: trunk/grails-app/controllers/nl/tno/metagenomics/integration/RestController.groovy @ 12

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

Implemented basic exporting functionality

File size: 20.6 KB
Line 
1package nl.tno.metagenomics.integration
2
3import grails.converters.*
4import nl.tno.metagenomics.Study
5import nl.tno.metagenomics.Sample
6import nl.tno.metagenomics.Assay
7
8import org.apache.catalina.connector.Response;
9import org.codehaus.groovy.grails.commons.ConfigurationHolder
10
11/** Expose the list of features within a certain assay
12 *
13 * @author Robert Horlings (robert@isdat.nl)
14 * @since 20101229
15 * @see   SAM.RestController
16 *
17 * $Rev$
18 *
19 * This class provides a REST-full service for getting and setting the data
20 * in the Metagenomics Module. The service consists of several
21 * resources listed below. So far, all resources are GET resoruces, i.e. we
22 * do not use PUT, POST or DELETE. Because we are using Grails' web libaries,
23 * each resource corresponds to exactly one action of this controller.
24 *
25 *
26 * The REST resources implemented in this controller are:
27 *
28 * metagenomics-host:port/metagenomics/rest/getMeasurements(assayToken)
29 * metagenomics-host:port/metagenomics/rest/getMeasurementMetadata(assayToken, measurementTokens)
30 * metagenomics-host:port/metagenomics/rest/getMeasurementData(assayToken, measurementTokens, sampleTokens)
31 * metagenomics-host:port/metagenomics/rest/getAssayURL(assayToken)
32 *
33 * Where 'metagenomics-host' is the url of the metagenomics server's host with port number 'port'.
34 *
35 */
36class RestController {
37        def synchronizationService
38       
39        /****************************************************************/
40        /* REST resource for handling study change in GSCF              */
41        /****************************************************************/
42       
43        /**
44         * Is called by GSCF when a study is added, changed or deleted.
45         * Sets the 'dirty' flag of a study to true, so that it will be updated
46         * next time the study is asked for.
47         *
48         * @param       studyToken
49         */
50        def notifyStudyChange = {
51                def studyToken = params.studyToken
52
53                if( !studyToken ) {
54                        response.sendError(400, "No studyToken given" )
55                        return
56                }
57
58                // Search for the changed study
59                def study = Study.findByStudyToken( studyToken );
60
61                // If the study is not found, it is added in GSCF. Add a dummy (dirty) study, in order to
62                // update it immediately when asked for
63                if( !study ) {
64                        log.info( "METAGENOMICS: GSCF notification for new study " + studyToken );
65                        study = new Study(
66                                        name: "",
67                                        studyToken: studyToken,
68                                        isDirty: true
69                                        )
70                } else {
71                        log.info( "METAGENOMICS: GSCF notification for existing study " + studyToken );
72                        study.isDirty = true;
73                }
74                study.save(flush:true);
75
76                def jsonData = [ 'studyToken': studyToken, message: "Notify succesful" ];
77
78                render jsonData as JSON
79        }
80
81        /**
82         * Return URL to view an assay.
83         *
84         * @param  assayToken
85         * @return URL to view an assay as single hash entry with key 'url'.
86         *
87         */
88        def getAssayURL = {
89                def assayToken = params.assayToken
90
91                if( !assayToken ) {
92                        render [] as JSON
93                        return
94                }
95
96                def assay = Assay.findByAssayToken( assayToken )
97
98                // If the assay is not found, try synchronizing
99                synchronizationService.sessionToken = session.sessionToken
100
101                if( !assay ) {
102                        synchronizationService.synchronizeStudies()
103                        assay = Assay.findByAssayToken( assayToken );
104
105                        if( !assay ) {
106                                response.sendError(404, "Not Found" )
107                                return;
108                        }
109                } else {
110                        try {
111                                synchronizationService.synchronizeAssay(assay);
112                        } catch( Exception e ) {
113                                response.sendError( 500, e.getMessage())
114                                return
115                        }
116
117                        def url = [ 'url' : ConfigurationHolder.config.grails.serverURL + '/assay/show/' + assay.id.toString() ]
118
119                        render url as JSON
120                }
121        }
122
123        /***************************************************/
124        /* REST resources related to the querying in GSCF  */
125        /***************************************************/
126
127        /**
128         * Retrieves a list of fields that could be queried when searching for a specific entity.
129         *
130         * The module is allowed to return different fields when the user searches for different entities
131         *
132         * Example call:                [moduleurl]/rest/getQueryableFields?entity=Study&entity=Sample
133         * Example response:    { "Study": [ "# sequences" ], "Sample": [ "# sequences", "# bacteria" ] }
134         *
135         * @param       params.entity   Entity that is searched for. Might be more than one. If no entity is given,
136         *                                                      a list of searchable fields for all entities is given
137         * @return      JSON                    List with the names of the fields
138         */
139        def getQueryableFields = {
140                // We don't really care about the entity. The only thing is that this module
141                // is only aware of studies, assays and samples, but doesn't know anything about
142                // subjects or events. If the user searches for those entities (maybe in the future)
143                // this module doesn't have anything to search for.
144
145                def entities = params.entity ?: []
146               
147                if( entities instanceof String )
148                        entities = [entities]
149                else
150                        entities = entities.toList()
151
152                if( !entities )
153                        entities = [ "Study", "Assay", "Sample" ]
154                       
155
156                def fields = [:];
157                entities.unique().each { entity -> 
158                        switch( entity ) {
159                                case "Study":
160                                case "Assay":
161                                case "Sample":
162                                        fields[ entity ] = [ "# sequences", "Tag name", "Run name" ]
163                                        break;
164                                default:
165                                        // Do nothing
166                                        break;
167                        }
168                }
169               
170                render fields as JSON
171        }
172       
173        /**
174         * Returns data for the given field and entities.
175         *
176         * Example call:                [moduleurl]/rest/getQueryableFieldData?entity=Study&tokens=abc1&tokens=abc2&fields=# sequences&fields=# bacteria
177         * Example response:    { "abc1": { "# sequences": 141, "# bacteria": 0 }, "abc2": { "#sequences": 412 } }
178         *
179         * @param       params.entity   Entity that is searched for
180         * @param       params.tokens   One or more tokens of the entities that the data should be returned for
181         * @param       params.fields   One or more field names of the data to be returned.
182         * @return      JSON                    Map with keys being the entity tokens and the values being maps with entries [field] = [value]. Not all
183         *                                                      fields and tokens that are asked for have to be returned by the module (e.g. when a specific entity can
184         *                                                      not be found, or a value is not present for an entity)
185         */
186        def getQueryableFieldData = {
187                def entity = params.entity;
188                def tokens = params.tokens ?: []
189                def fields = params.fields ?: []
190
191                if( tokens instanceof String )
192                        tokens = [tokens]
193                else
194                        tokens = tokens.toList();
195
196                if( fields instanceof String )
197                        fields = [fields]
198                else
199                        fields = fields.toList();
200
201                // Only search for unique tokens and fields
202                tokens = tokens.unique()
203                fields = fields.unique()
204
205                // Without tokens or fields we can only return an empty list
206                def map = [:]
207                if( tokens.size() == 0 || fields.size() == 0 ) {
208                        log.trace "Return empty string for getQueryableFieldData: #tokens: " + tokens.size() + " #fields: " + fields.size()
209                        render map as JSON
210                        return;
211                }
212
213                for( token in tokens ) {
214                        def object = getQueryableObject( entity, token );
215
216                        if( object ) {
217                                // Check whether the user has sufficient privileges:
218                                def study;
219                                switch( entity ) {
220                                        case "Study":
221                                                study = object; 
222                                                break;
223                                        case "Assay":
224                                        case "Sample":
225                                                study = object.study
226                                                break;
227                                        default:
228                                                log.error "Incorrect entity used: " + entity;
229                                                continue;
230                                }
231                               
232                                if( !study.canRead( session.user ) ) {
233                                        log.error "Data was requested for " + entity.toLowerCase() + " " + object.name + " but the user " + session.user.username + " doesn't have the right privileges."
234                                        continue;
235                                } else {
236                                        log.error "Data was requested for " + entity.toLowerCase() + " " + object.name + ": " + session.user.username + " - " + study.canRead( session.user )
237                                }
238                               
239                                map[ token ] = [:]
240                                fields.each { field ->
241                                        def v = getQueryableFieldValue( entity, object, field );
242                                        if( v != null )
243                                                map[ token ][ field ] = v
244                                }
245                        } else {
246                                log.trace "No " + entity + " with token " + token + " found."
247                        }
248                }
249               
250                render map as JSON
251        }
252
253        /**
254         * Searches for a specific entity
255         *
256         * @param entity                Entity to search in
257         * @param token         Token of the entity to search in
258         * @return
259         */
260        protected def getQueryableObject( def entity, def token ) {
261                switch( entity ) {
262                        case "Study":
263                                return Study.findByStudyToken( token );
264                        case "Assay":
265                                return Assay.findByAssayToken( token );
266                        case "Sample":
267                                return Sample.findBySampleToken( token );
268                        default:
269                        // Other entities can't be handled
270                                return null;
271                }
272        }
273
274        /**
275         * Searches for the value of a specific field in a specific entity
276         *
277         * @param entity        Entity of the given object
278         * @param object        Object to search in
279         * @param field         Field value to retrieve         
280         * @return
281         */
282        protected def getQueryableFieldValue( def entity, def object, def field ) {
283                if( !entity || !object || !field )
284                        return null;
285                       
286                // First determine all assaysamples involved, in order to return data later.
287                // All data that has to be returned is found in assaysamples
288                def assaySamples
289               
290                switch( entity ) {
291                        case "Study":
292                                assaySamples = object.assays*.assaySamples;
293                                if( assaySamples ) {
294                                        assaySamples = assaySamples.flatten()
295                                } 
296                                break;
297                        case "Assay":
298                        case "Sample":
299                                assaySamples = object.assaySamples;
300                                break;
301                        default:
302                        // Other entities can't be handled
303                                return null;
304                }
305               
306                // If no assaySamples are involved, return null as empty value
307                if( !assaySamples ) {
308                        return null;
309                }
310               
311                // Now determine the exact field to return
312                switch( field ) {
313                        // Returns the total number of sequences in this sample
314                        case "# sequences":
315                                return assaySamples.collect { it.numSequences() }.sum();
316                        // Returns the unique tag names
317                        case "Tag name":
318                                return assaySamples.collect { it.tagName }.findAll { it != null }.unique();
319                        case "Run name":
320                                return assaySamples.collect { it.run?.name }.findAll { it != null }.unique();
321                        // Other fields are not handled
322                        default:
323                                return null;
324                }
325
326        }
327       
328        private def checkAssayToken( def assayToken ) {
329                if( !assayToken || assayToken == null ) {
330                        return false
331                }
332                def list = []
333                def assay = Assay.findByAssayToken( assayToken )
334
335                if( !assay || assay == null ) {
336                        return false;
337                }
338
339                return assay;
340        }
341
342        /****************************************************************/
343        /* REST resources for exporting data from GSCF                          */
344        /****************************************************************/
345
346        /**
347         * Retrieves a list of actions that can be performed on data with a specific entity.
348         *
349         * The module is allowed to return different fields when the user searches for different entities
350         *
351         * Example call:                [moduleurl]/rest/getPossibleActions?entity=Assay&entity=Sample
352         * Example response:    { "Assay": [ { name: "excel", description: "Export as excel" } ],
353         *                                                "Sample": [ { name: "excel", description: "Export as excel" }, { name: "fasta", description: : "Export as fasta" } ] }
354         *
355         * @param       params.entity   Entity that is searched for. Might be more than one. If no entity is given,
356         *                                                      a list of searchable fields for all entities is given
357         * @return      JSON                    Hashmap with keys being the entities and the values are lists with the action this module can
358         *                                                      perform on this entity. The actions as hashmaps themselves, with keys 'name' and 'description'
359         */
360        def getPossibleActions = {
361                def entities = params.entity ?: []
362               
363                if( entities instanceof String )
364                        entities = [entities]
365                else
366                        entities = entities.toList()
367
368                if( !entities )
369                        entities = [ "Study", "Assay", "Sample" ]
370
371                def actions = [:];
372                entities.unique().each { entity ->
373                        switch( entity ) {
374                                case "Study":
375                                        actions[ entity ] = [ [ name: "excel", description: "Export as excel" ] ]
376                                        break;
377                                case "Assay":
378                                        actions[ entity ] = [ [ name: "fasta", description: "Export as fasta" ] ]
379                                        break;
380                                case "Sample":
381                                        actions[ entity ] = [ [ name: "fasta", description: "Export as fasta" ] ]
382                                        break;
383                                default:
384                                        // Do nothing
385                                        break;
386                        }
387                }
388               
389                render actions as JSON
390        }
391
392        /****************************************************************/
393        /* REST resources for providing basic data to the GSCF          */
394        /****************************************************************/
395        private getMeasurementTypes() {
396                return [ "# sequences" ]
397        }
398
399        /**
400         * Return a list of simple assay measurements matching the querying text.
401         *
402         * @param assayToken
403         * @return list of measurements for token. Each member of the list is a hash.
404         *                      the hash contains the three keys values pairs: value, sampleToken, and
405         *                      measurementMetadata.
406         *
407         * Example REST call:
408         * http://localhost:8184/metagenomics/rest/getMeasurements/query?assayToken=16S-5162
409         *
410         * Resulting JSON object:
411         *
412         * [ "# sequences", "average quality" ]
413         *
414         */
415        def getMeasurements = {
416                def assayToken = params.assayToken;
417
418                if( !checkAssayToken( assayToken ) ) {
419                        response.sendError(404)
420                        return false
421                }
422
423                render getMeasurementTypes() as JSON
424        }
425
426        /**
427         * Return measurement metadata for measurement
428         *
429         * @param assayToken
430         * @param measurementTokens. List of measurements for which the metadata is returned.
431         *                           If this is not given, then return metadata for all
432         *                           measurements belonging to the specified assay.
433         * @return list of measurements
434         *
435         * Example REST call:
436         * http://localhost:8184/metagenomics/rest/getMeasurementMetadata/query?assayToken=16S-5162
437         *      &measurementToken=# sequences
438         *              &measurementToken=average quality
439         *
440         * Example resulting JSON object:
441         *
442         * [ {"name":"# sequences","type":"raw"},
443         *   {"name":"average quality", "unit":"Phred"} ]
444         */
445        def getMeasurementMetadata = {
446                def assayToken = params.assayToken
447                def measurementTokens = params.measurementToken
448
449                if( !checkAssayToken( assayToken ) ) {
450                        response.sendError(404)
451                        return false
452                }
453
454                // For now, we don't have any metadata about the measurements
455                def measurements = getMeasurementTypes();
456                def measurementMetadata = []
457
458                measurementTokens.each { token ->
459                        if( measurements.contains( token ) )
460                                measurementMetadata << [ "name" : token ];
461                }
462
463                render measurementMetadata as JSON
464        }
465
466        /**
467         * Return list of measurement data.
468         *
469         * @param assayTokes
470         * @param measurementToken. Restrict the returned data to the measurementTokens specified here.
471         *                                              If this argument is not given, all samples for the measurementTokens are returned.
472         *                                              Multiple occurences of this argument are possible.
473         * @param sampleToken. Restrict the returned data to the samples specified here.
474         *                                              If this argument is not given, all samples for the measurementTokens are returned.
475         *                                              Multiple occurences of this argument are possible.
476         * @param boolean verbose. If this argument is not present or it's value is true, then return
477         *                      the date in a redundant format that is easier to process.
478         *                                              By default, return a more compact JSON object as follows.
479         *
480         *                                              The list contains three elements:
481         *
482         *                                              (1) a list of sampleTokens,
483         *                                              (2) a list of measurementTokens,
484         *                                              (3) a list of values.
485         *
486         *                                              The list of values is a matrix represented as a list. Each row of the matrix
487         *                                              contains the values of a measurementToken (in the order given in the measurement
488         *                                              token list, (2)). Each column of the matrix contains the values for the sampleTokens
489         *                                              (in the order given in the list of sampleTokens, (1)).
490         *                                              (cf. example below.)
491         *
492         *
493         * @return  table (as hash) with values for given samples and measurements
494         *
495         *
496         * List of examples.
497         *
498         *
499         * Example REST call:
500         * http://localhost:8184/metagenomics/rest/getMeasurementData/doit?assayToken=PPSH-Glu-A
501         *    &measurementToken=total carbon dioxide (tCO)
502         *    &sampleToken=5_A
503         *    &sampleToken=1_A
504         *    &verbose=true
505         *
506         * Resulting JSON object:
507         * [ {"sampleToken":"1_A","measurementToken":"total carbon dioxide (tCO)","value":28},
508         *   {"sampleToken":"5_A","measurementToken":"total carbon dioxide (tCO)","value":29} ]
509         *
510         *
511         *
512         * Example REST call without sampleToken, without measurementToken,
513         *    and with verbose representation:
514         * http://localhost:8184/metagenomics/rest/getMeasurementData/dossit?assayToken=PPSH-Glu-A
515         *    &verbose=true
516         *
517         * Resulting JSON object:
518         * [ {"sampleToken":"1_A","measurementToken":"sodium (Na+)","value":139},
519         *       {"sampleToken":"1_A","measurementToken":"potassium (K+)","value":4.5},
520         *       {"sampleToken":"1_A","measurementToken":"total carbon dioxide (tCO)","value":26},
521         *       {"sampleToken":"2_A","measurementToken":"sodium (Na+)","value":136},
522         *       {"sampleToken":"2_A","measurementToken":"potassium (K+)","value":4.3},
523         *       {"sampleToken":"2_A","measurementToken":"total carbon dioxide (tCO)","value":28},
524         *       {"sampleToken":"3_A","measurementToken":"sodium (Na+)","value":139},
525         *       {"sampleToken":"3_A","measurementToken":"potassium (K+)","value":4.6},
526         *       {"sampleToken":"3_A","measurementToken":"total carbon dioxide (tCO)","value":27},
527         *       {"sampleToken":"4_A","measurementToken":"sodium (Na+)","value":137},
528         *       {"sampleToken":"4_A","measurementToken":"potassium (K+)","value":4.6},
529         *       {"sampleToken":"4_A","measurementToken":"total carbon dioxide (tCO)","value":26},
530         *       {"sampleToken":"5_A","measurementToken":"sodium (Na+)","value":133},
531         *       {"sampleToken":"5_A","measurementToken":"potassium (K+)","value":4.5},
532         *       {"sampleToken":"5_A","measurementToken":"total carbon dioxide (tCO)","value":29} ]
533         *
534         *
535         *
536         * Example REST call with default (non-verbose) view and without sampleToken:
537         *
538         * Resulting JSON object:
539         * http://localhost:8184/metagenomics/rest/getMeasurementData/query?
540         *      assayToken=PPSH-Glu-A&
541         *      measurementToken=total carbon dioxide (tCO)
542         *
543         * Resulting JSON object:
544         * [ ["1_A","2_A","3_A","4_A","5_A"],
545         *   ["sodium (Na+)","potassium (K+)","total carbon dioxide (tCO)"],
546         *   [139,136,139,137,133,4.5,4.3,4.6,4.6,4.5,26,28,27,26,29] ]
547         *
548         * Explanation:
549         * The JSON object returned by default (i.e., unless verbose is set) is an array of three arrays.
550         * The first nested array gives the sampleTokens for which data was retrieved.
551         * The second nested array gives the measurementToken for which data was retrieved.
552         * The thrid nested array gives the data for sampleTokens and measurementTokens.
553         *
554         *
555         * In the example, the matrix represents the values of the above Example and
556         * looks like this:
557         *
558         *                      1_A             2_A             3_A             4_A             5_A
559         *
560         * Na+          139             136             139             137             133
561         *
562         * K+           4.5             4.3             4.6             4.6             4.5
563         *
564         * tCO          26              28              27              26              29
565         *
566         */
567        def getMeasurementData = {
568                def assayToken = params.assayToken
569                def measurementTokens = params.measurementToken
570                def sampleTokens = params.sampleToken
571                def verbose = false
572
573                if(params.verbose && (params.verbose=='true'||params.verbose==true) ) {
574                        verbose=true
575                }
576
577                def assay = checkAssayToken( assayToken );
578                if( !assay ) {
579                        response.sendError(404)
580                        return false
581                }
582
583                if( !measurementTokens ) {
584                        measurementTokens = []
585                }
586                else if( measurementTokens.class == java.lang.String ) {
587                        measurementTokens = [ measurementTokens ]
588                }
589
590                if( !sampleTokens ) {
591                        sampleTokens = []
592                }
593                else if( sampleTokens.class == java.lang.String ) {
594                        sampleTokens = [ sampleTokens ]
595                }
596
597                def data = SampleAssay.findAllByAssay( assay );
598                def measurements = getMeasurementTypes()
599
600                def results = []
601                data.each { sampleAssay ->
602                        measurements.each { type ->
603                                def sample = sampleAssay.sample.sampleToken
604                                def isMatch = false
605
606                                // Check if this measurement should be returned
607                                if( ( measurementTokens.isEmpty() || measurementTokens.contains( type ) ) &&
608                                ( sampleTokens.isEmpty()      || sampleTokens.contains( sample ) ) ) {
609                                        results.push( [ 'sampleToken': sample, 'measurementToken': type, 'value': sampleAssay[ type ] ] )
610                                }
611                        }
612                }
613
614                if(!verbose) {
615                        results = compactTable( results )
616                }
617
618                render results as JSON
619        }
620
621       
622        /* helper function for getSamples
623         *
624         * Return compact JSON object for data. The format of the returned array is as follows.
625         *
626         * The list contains three elements:
627         *
628         * (1) a list of sampleTokens,
629         * (2) a list of measurementTokens,
630         * (3) a list of values.
631         *
632         * The list of values is a matrix represented as a list. Each row of the matrix
633         * contains the values of a measurementToken (in the order given in the measurement
634         * token list, (2)). Each column of the matrix contains the values for the sampleTokens
635         * (in the order given in the list of sampleTokens, (1)).
636         */
637        def compactTable( results ) {
638                def i = 0
639                def sampleTokenIndex = [:]
640                def sampleTokens = results.collect( { it['sampleToken'] } ).unique()
641                sampleTokens.each{ sampleTokenIndex[it] = i++ }
642
643                i = 0
644                def measurementTokenIndex= [:]
645                def measurementTokens = results.collect( { it['measurementToken'] } ).unique()
646                measurementTokens.each{ measurementTokenIndex[it] = i++ }
647
648                def data = []
649                measurementTokens.each{ m ->
650                        sampleTokens.each{ s ->
651                                def item = results.find{ it['sampleToken']==s && it['measurementToken']==m }
652                                data.push item ? item['value'] : null
653                        }
654                }
655
656                return [ sampleTokens, measurementTokens, data ]
657        }
658
659}
Note: See TracBrowser for help on using the repository browser.