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

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

Improved querying (#40)

  • Removed continuous synchronization while importing data
  • Added action buttons to search result screen
  • Implemented authorization checks while searching
File size: 24.4 KB
Line 
1package nl.tno.massSequencing.integration
2
3import grails.converters.*
4import nl.tno.massSequencing.*
5
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        def queryService
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", "Classification" ]
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                log.debug "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                def start = System.currentTimeMillis();
222                def lap
223                 
224                for( token in tokens ) {
225                        lap = System.currentTimeMillis();
226                         
227                        def object = getQueryableObject( entity, token );
228
229                        if( object ) {
230                                // Check whether the user has sufficient privileges:
231                                def study;
232                                switch( entity ) {
233                                        case "Study":
234                                                study = object; 
235                                                break;
236                                        case "Assay":
237                                        case "Sample":
238                                                study = object.study
239                                                break;
240                                        default:
241                                                log.error "Incorrect entity used: " + entity;
242                                                continue;
243                                }
244                               
245                                if( !study.canRead( session.user ) ) {
246                                        log.error "Data was requested for " + entity.toLowerCase() + " " + object.name + " but the user " + session.user.username + " doesn't have the right privileges."
247                                        continue;
248                                }
249                               
250                                map[ token ] = [:]
251                                fields.each { field ->
252                                        def v = queryService.getQueryableFieldValue( entity, object, field );
253                                        if( v != null )
254                                                map[ token ][ field ] = v
255                                }
256                        } else {
257                                log.trace "No " + entity + " with token " + token + " found."
258                        }
259                }
260                 
261                render map as JSON
262        }
263       
264        /**
265         * Searches for a specific entity
266         *
267         * @param entity                Entity to search in
268         * @param token         Token of the entity to search in
269         * @return
270         */
271        protected def getQueryableObject( def entity, def token ) {
272                switch( entity ) {
273                        case "Study":
274                                return Study.findByStudyToken( token );
275                        case "Assay":
276                                return Assay.findByAssayToken( token );
277                        case "Sample":
278                                return Sample.findBySampleToken( token );
279                        default:
280                        // Other entities can't be handled
281                                return null;
282                }
283        }
284
285        /**
286         * Searches for the value of a specific field in a specific entity
287         *
288         * @param entity        Entity of the given object
289         * @param object        Object to search in
290         * @param field         Field value to retrieve         
291         * @return
292         */
293        protected def getQueryableFieldValue( def entity, def object, def field ) {
294                if( !entity || !object || !field )
295                        return null;
296                       
297                // First determine all assaysamples involved, in order to return data later.
298                // All data that has to be returned is found in assaysamples
299                def assaySamples
300               
301                def where = [];
302                def from = "FROM AssaySample a"
303               
304                switch( entity ) {
305                        case "Study":
306                                where << "a.assay.study = :object";
307                               
308                                assaySamples = object.assays*.assaySamples;
309                                if( assaySamples ) {
310                                        assaySamples = assaySamples.flatten()
311                                } 
312                                break;
313                        case "Assay":
314                                where << " a.assay = :object ";
315                        case "Sample":
316                                where << " a.sample == :sample ";
317                                assaySamples = object.assaySamples;
318                                break;
319                        default:
320                        // Other entities can't be handled
321                                return null;
322                }
323               
324                // If no assaySamples are involved, return null as empty value
325                if( !assaySamples ) {
326                        return null;
327                }
328               
329                // Now determine the exact field to return
330                switch( field ) {
331                        // Returns the total number of sequences in this sample
332                        case "# sequences":
333                                return assaySamples.collect { it.numSequences() }.sum();
334                        // Returns the unique tag names
335                        case "Forward primer":
336                                return assaySamples.collect { it.fwPrimerSeq }.findAll { it != null }.unique();
337                        case "Mid name":
338                                return assaySamples.collect { it.fwMidName }.findAll { it != null }.unique();
339                        case "Oligo number":
340                                return assaySamples.collect { it.fwOligo }.findAll { it != null }.unique();
341                        case "Run name":
342                                return assaySamples.collect { it.run?.name }.findAll { it != null }.unique();
343                        case "Classification":
344                                // Return the names of all species that have been found in the given sample
345                               
346                        // Other fields are not handled
347                        default:
348                                return null;
349                }
350
351        }
352       
353       
354       
355        private def checkAssayToken( def assayToken ) {
356                if( !assayToken || assayToken == null ) {
357                        return false
358                }
359                def list = []
360                def assay = Assay.findByAssayToken( assayToken )
361
362                if( !assay || assay == null ) {
363                        return false;
364                }
365
366                return assay;
367        }
368
369        /****************************************************************/
370        /* REST resources for exporting data from GSCF                          */
371        /****************************************************************/
372
373        /**
374         * Retrieves a list of actions that can be performed on data with a specific entity. This includes actions that
375         * refine the search result.
376         *
377         * The module is allowed to return different fields when the user searches for different entities
378         *
379         * Example call:                [moduleurl]/rest/getPossibleActions?entity=Assay&entity=Sample
380         * Example response:    { "Assay": [ { name: "excel", description: "Export as excel" } ],
381         *                                                "Sample": [ { name: "excel", description: "Export as excel" }, { name: "fasta", description: : "Export as fasta" } ] }
382         *
383         * @param       params.entity   Entity that is searched for. Might be more than one. If no entity is given,
384         *                                                      a list of searchable fields for all entities is given
385         * @return      JSON                    Hashmap with keys being the entities and the values are lists with the action this module can
386         *                                                      perform on this entity. The actions as hashmaps themselves, with keys
387         *                                                      'name'                  Unique name of the action, as used for distinguishing actions
388         *                                                      'description'   Human readable description
389         *                                                      'url'                   URL to send the user to when performing this action. The user is sent there using POST with
390         *                                                                                      the following parameters:
391         *                                                                                              actionName:             Name of the action to perform
392         *                                                                                              name:                   Name of the search that the action resulted from
393         *                                                                                              url:                    Url of the search that the action resulted from
394         *                                                                                              entity:                 Type of entity being returned
395         *                                                                                              tokens:                 List of entity tokens
396         *                                                      'type'                  (optional) Determines what type of action it is. Possible values: 'default', 'refine', 'export', ''
397         */
398        def getPossibleActions = {
399                def entities = params.entity ?: []
400               
401                if( entities instanceof String )
402                        entities = [entities]
403                else
404                        entities = entities.toList()
405
406                if( !entities )
407                        entities = [ "Study", "Assay", "Sample" ]
408
409                def actions = [:];
410                entities.unique().each { entity ->
411                        switch( entity ) {
412                                case "Study":
413                                        actions[ entity ] = [ 
414                                                [ name: "excel", type: 'export', description: "Export metadata", url: createLink( controller: "study", action: "exportMetaData", absolute: true ) ], 
415                                                [ name: "fasta", type: 'export', description: "Export as fasta", url: createLink( controller: "study", action: "exportAsFasta", absolute: true ) ], 
416                                                [ name: "refine", type: 'refine', description: "Refine by classification", url: createLink( controller: "query", action: "refineExternal", absolute: true ) ], 
417                                        ]
418                                        break;
419                                case "Assay":
420                                        actions[ entity ] = [ 
421                                                [ name: "fasta", type: 'export', description: "Export as fasta", url: createLink( controller: "assay", action: "exportAsFasta", absolute: true ) ],
422                                                [ name: "excel", type: 'export', description: "Export metadata", url: createLink( controller: "assay", action: "exportMetaData", absolute: true ) ],
423                                                [ name: "refine", type: 'refine', description: "Refine by classification", url: createLink( controller: "query", action: "refineExternal", absolute: true ) ], 
424                                        ]
425                                        break;
426                                case "Sample":
427                                        actions[ entity ] = [ 
428                                                [ name: "fasta", type: 'export', description: "Export as fasta", url: createLink( controller: "sample", action: "exportAsFasta", absolute: true ) ], 
429                                                [ name: "excel", type: 'export', description: "Export metadata", url: createLink( controller: "sample", action: "exportMetaData", absolute: true ) ], 
430                                                [ name: "refine", type: 'refine', description: "Refine by classification", url: createLink( controller: "query", action: "refineExternal", absolute: true ) ], 
431                                        ]
432                                        break;
433                                default:
434                                        // Do nothing
435                                        break;
436                        }
437                }
438               
439                render actions as JSON
440        }
441       
442        /****************************************************************/
443        /* REST resources for providing basic data to the GSCF          */
444        /****************************************************************/
445        private getMeasurementTypes() {
446                return [ 
447                        "# sequences": null, "# qual scores": null, 
448                        "run name": null, 
449                        "forward oligo number":         "fwOligo", 
450                        "forward mid name":             "fwMidName", 
451                        "forward total sequence":       "fwTotalSeq", 
452                        "forward mid sequence":         "fwMidSeq", 
453                        "forward primer sequence":      "fwMidSeq",
454                        "reverse oligo number":         "revOligo", 
455                        "reverse mid name":             "revMidName", 
456                        "reverse total sequence":       "revTotalSeq", 
457                        "reverse mid sequence":         "revMidSeq", 
458                        "reverse primer sequence":      "revMidSeq"
459                ]
460        }
461
462        /**
463         * Return a list of simple assay measurements matching the querying text.
464         *
465         * @param assayToken
466         * @return list of measurements for token. Each member of the list is a hash.
467         *                      the hash contains the three keys values pairs: value, sampleToken, and
468         *                      measurementMetadata.
469         *
470         * Example REST call:
471         * http://localhost:8184/metagenomics/rest/getMeasurements/query?assayToken=16S-5162
472         *
473         * Resulting JSON object:
474         *
475         * [ "# sequences", "average quality" ]
476         *
477         */
478        def getMeasurements = {
479                def assayToken = params.assayToken;
480
481                if( !checkAssayToken( assayToken ) ) {
482                        response.sendError(404)
483                        return false
484                }
485               
486                render getMeasurementTypes().keySet().asList() as JSON
487        }
488
489        /**
490         * Return measurement metadata for measurement
491         *
492         * @param assayToken
493         * @param measurementTokens. List of measurements for which the metadata is returned.
494         *                           If this is not given, then return metadata for all
495         *                           measurements belonging to the specified assay.
496         * @return list of measurements
497         *
498         * Example REST call:
499         * http://localhost:8184/metagenomics/rest/getMeasurementMetadata/query?assayToken=16S-5162
500         *      &measurementToken=# sequences
501         *              &measurementToken=average quality
502         *
503         * Example resulting JSON object:
504         *
505         * [ {"name":"# sequences","type":"raw"},
506         *   {"name":"average quality", "unit":"Phred"} ]
507         */
508        def getMeasurementMetaData = {
509                def assayToken = params.assayToken
510                def measurementTokens = params.measurementToken
511               
512                if( !checkAssayToken( assayToken ) ) {
513                        response.sendError(404)
514                        return false
515                }
516
517                // For now, we don't have any metadata about the measurements
518                def measurements = getMeasurementTypes().keySet().asList();
519                def measurementMetadata = [ ]
520
521                // If no measurementTokens are given, all measurements are returned
522                if( !measurementTokens )
523                        measurementTokens = measurements
524               
525                measurementTokens.each { token ->
526                        if( measurements.contains( token ) )
527                                measurementMetadata << [ "name" : token ];
528                }
529
530                render measurementMetadata as JSON
531        }
532
533        /**
534         * Return list of measurement data.
535         *
536         * @param assayTokes
537         * @param measurementToken. Restrict the returned data to the measurementTokens specified here.
538         *                                              If this argument is not given, all samples for the measurementTokens are returned.
539         *                                              Multiple occurences of this argument are possible.
540         * @param sampleToken. Restrict the returned data to the samples specified here.
541         *                                              If this argument is not given, all samples for the measurementTokens are returned.
542         *                                              Multiple occurences of this argument are possible.
543         * @param boolean verbose. If this argument is not present or it's value is true, then return
544         *                      the date in a redundant format that is easier to process.
545         *                                              By default, return a more compact JSON object as follows.
546         *
547         *                                              The list contains three elements:
548         *
549         *                                              (1) a list of sampleTokens,
550         *                                              (2) a list of measurementTokens,
551         *                                              (3) a list of values.
552         *
553         *                                              The list of values is a matrix represented as a list. Each row of the matrix
554         *                                              contains the values of a measurementToken (in the order given in the measurement
555         *                                              token list, (2)). Each column of the matrix contains the values for the sampleTokens
556         *                                              (in the order given in the list of sampleTokens, (1)).
557         *                                              (cf. example below.)
558         *
559         *
560         * @return  table (as hash) with values for given samples and measurements
561         *
562         *
563         * List of examples.
564         *
565         *
566         * Example REST call:
567         * http://localhost:8184/metagenomics/rest/getMeasurementData/doit?assayToken=PPSH-Glu-A
568         *    &measurementToken=total carbon dioxide (tCO)
569         *    &sampleToken=5_A
570         *    &sampleToken=1_A
571         *    &verbose=true
572         *
573         * Resulting JSON object:
574         * [ {"sampleToken":"1_A","measurementToken":"total carbon dioxide (tCO)","value":28},
575         *   {"sampleToken":"5_A","measurementToken":"total carbon dioxide (tCO)","value":29} ]
576         *
577         *
578         *
579         * Example REST call without sampleToken, without measurementToken,
580         *    and with verbose representation:
581         * http://localhost:8184/metagenomics/rest/getMeasurementData/dossit?assayToken=PPSH-Glu-A
582         *    &verbose=true
583         *
584         * Resulting JSON object:
585         * [ {"sampleToken":"1_A","measurementToken":"sodium (Na+)","value":139},
586         *       {"sampleToken":"1_A","measurementToken":"potassium (K+)","value":4.5},
587         *       {"sampleToken":"1_A","measurementToken":"total carbon dioxide (tCO)","value":26},
588         *       {"sampleToken":"2_A","measurementToken":"sodium (Na+)","value":136},
589         *       {"sampleToken":"2_A","measurementToken":"potassium (K+)","value":4.3},
590         *       {"sampleToken":"2_A","measurementToken":"total carbon dioxide (tCO)","value":28},
591         *       {"sampleToken":"3_A","measurementToken":"sodium (Na+)","value":139},
592         *       {"sampleToken":"3_A","measurementToken":"potassium (K+)","value":4.6},
593         *       {"sampleToken":"3_A","measurementToken":"total carbon dioxide (tCO)","value":27},
594         *       {"sampleToken":"4_A","measurementToken":"sodium (Na+)","value":137},
595         *       {"sampleToken":"4_A","measurementToken":"potassium (K+)","value":4.6},
596         *       {"sampleToken":"4_A","measurementToken":"total carbon dioxide (tCO)","value":26},
597         *       {"sampleToken":"5_A","measurementToken":"sodium (Na+)","value":133},
598         *       {"sampleToken":"5_A","measurementToken":"potassium (K+)","value":4.5},
599         *       {"sampleToken":"5_A","measurementToken":"total carbon dioxide (tCO)","value":29} ]
600         *
601         *
602         *
603         * Example REST call with default (non-verbose) view and without sampleToken:
604         *
605         * Resulting JSON object:
606         * http://localhost:8184/metagenomics/rest/getMeasurementData/query?
607         *      assayToken=PPSH-Glu-A&
608         *      measurementToken=total carbon dioxide (tCO)
609         *
610         * Resulting JSON object:
611         * [ ["1_A","2_A","3_A","4_A","5_A"],
612         *   ["sodium (Na+)","potassium (K+)","total carbon dioxide (tCO)"],
613         *   [139,136,139,137,133,4.5,4.3,4.6,4.6,4.5,26,28,27,26,29] ]
614         *
615         * Explanation:
616         * The JSON object returned by default (i.e., unless verbose is set) is an array of three arrays.
617         * The first nested array gives the sampleTokens for which data was retrieved.
618         * The second nested array gives the measurementToken for which data was retrieved.
619         * The thrid nested array gives the data for sampleTokens and measurementTokens.
620         *
621         *
622         * In the example, the matrix represents the values of the above Example and
623         * looks like this:
624         *
625         *                      1_A             2_A             3_A             4_A             5_A
626         *
627         * Na+          139             136             139             137             133
628         *
629         * K+           4.5             4.3             4.6             4.6             4.5
630         *
631         * tCO          26              28              27              26              29
632         *
633         */
634        def getMeasurementData = {
635                def assayToken = params.assayToken
636                def measurementTokens = params.measurementToken
637                def sampleTokens = params.sampleToken
638                def verbose = false
639
640                if(params.verbose && (params.verbose=='true'||params.verbose==true) ) {
641                        verbose=true
642                }
643
644                def assay = checkAssayToken( assayToken );
645                if( !assay ) {
646                        response.sendError(404)
647                        return false
648                }
649
650                // Check which measurement tokens are asked for
651                if( !measurementTokens ) {
652                        measurementTokens = []
653                } else if( measurementTokens.class == java.lang.String ) {
654                        measurementTokens = [ measurementTokens ]
655                } else if( measurementTokens.class == java.lang.String[] ) {
656                        measurementTokens = measurementTokens.toList();
657                }
658
659                // Check which sample tokens are asked for
660                if( !sampleTokens ) {
661                        sampleTokens = []
662                } else if( sampleTokens.class == java.lang.String ) {
663                        sampleTokens = [ sampleTokens ]
664                } else if( sampleTokens.class == java.lang.String[] ) {
665                        sampleTokens = sampleTokens.toList();
666                }
667
668                def data = AssaySample.findAllByAssay( assay );
669                def measurements = getMeasurementTypes()
670
671                def results = []
672                data.each { assaySample ->
673                        measurements.each { type ->
674                                def sample = assaySample.sample.sampleToken
675                                def isMatch = false
676
677                                // Check if this measurement should be returned
678                                if( 
679                                        ( measurementTokens.size() == 0 || measurementTokens.contains( type.key ) ) &&
680                                        ( sampleTokens.size() == 0      || sampleTokens.contains( sample ) ) ) {
681                                       
682                                        def value
683                                        if( type.value == null ) {
684                                                switch( type.key ) {
685                                                        case "# sequences":
686                                                                value = assaySample.numSequences(); break;
687                                                        case "# qual scores":
688                                                                value = assaySample.numQualScores(); break;
689                                                        case "run name":
690                                                                value = assaySample.run?.name; break;
691                                                }
692                                        } else {
693                                                value = assaySample[ type.value ];
694                                        }
695                                       
696                                        results.push( [ 'sampleToken': sample, 'measurementToken': type.key, 'value': value ] )
697                                }
698                        }
699                }
700
701                if(!verbose) {
702                        results = compactTable( results )
703                }
704
705                render results as JSON
706        }
707
708       
709        /* helper function for getSamples
710         *
711         * Return compact JSON object for data. The format of the returned array is as follows.
712         *
713         * The list contains three elements:
714         *
715         * (1) a list of sampleTokens,
716         * (2) a list of measurementTokens,
717         * (3) a list of values.
718         *
719         * The list of values is a matrix represented as a list. Each row of the matrix
720         * contains the values of a measurementToken (in the order given in the measurement
721         * token list, (2)). Each column of the matrix contains the values for the sampleTokens
722         * (in the order given in the list of sampleTokens, (1)).
723         */
724        def compactTable( results ) {
725                def sampleTokens = results.collect( { it['sampleToken'] } ).unique()
726                def measurementTokens = results.collect( { it['measurementToken'] } ).unique()
727
728                def data = []
729                measurementTokens.each{ m ->
730                        sampleTokens.each{ s ->
731                                def item = results.find{ it['sampleToken']==s && it['measurementToken']==m }
732                                data.push item ? item['value'] : null
733                        }
734                }
735
736                return [ sampleTokens, measurementTokens, data ]
737        }
738
739}
Note: See TracBrowser for help on using the repository browser.