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

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