source: trunk/grails-app/controllers/nl/tno/massSequencing/integration/RestController.groovy @ 46

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

Fixed bug in updating samples and improved rest.getQueryableFieldData to return all fields if no fields are given.

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