root/src/groovy/dbnp/rest/common/CommunicationManager.groovy @ 146

Revision 146, 11.5 KB (checked in by j.saito@…, 4 years ago)

Removed three possible sources for null-pointer exceptions in the CommunicationManager?.

Added a simpel way of handling REST errors using a new RestException? class.

I have tried to introduce Exception handling for errors in the Communication manager. So far, I only committed exceptions for something going wrong. I have been working on a more fine grained distinction between server side errors and client side errors related to REST. however, these errors require some sort of convention for delivering from the server side to the client side. Therefore, they require a convention to be agreed to by NMCDSP and I have not committed these Exceptions.

Line 
1package dbnp.rest.common
2
3import dbnp.rest.*
4import grails.converters.JSON
5import java.net.URLEncoder
6import org.codehaus.groovy.grails.web.json.*
7
8/**  CommunicationManager
9 *
10 *   @author Jahn
11 *
12 *   This class manages communication between dbNP modules such as GSCF and SAM.
13 *   By communication we mean two ways of exchanging information: (1) via Rest resources,
14 *   and (2) via Grails views that a module can make available to another module.
15 *
16 *   For Rest communication this class implements a Rest client that fetches data
17 *   from other modules' Rest resources. The Rest implementation transfers data in JSON.
18 *
19 *   Note: Do not use this class directly to fetch data. Instead use your module's
20 *   rest wrapper methods. Use this module, to create these rest wrapper methods.
21 *   For instance, use dbnp.rest.sam.registerRestWrapperMethodsGSCFtoSAM to register new methods
22 *   for accessing GSCF's Rest service in SAM; your new method shoud then use this class.
23 */
24
25
26class CommunicationManager {
27
28    def        static Encoding      = "UTF-8"
29    def public static SAMServerURL  = "http://localhost:8182/sam"
30    def public static GSCFServerURL = "http://localhost:8080/gscf"
31
32    static def username = "user"
33        static def password = "useR123!"
34
35    /**
36     * Get the results of provided by a rest Rest resource.
37     *
38     * @params String resource The name of the resource, e.g. importer/pages
39     * @params Map params      A Map of parmater names and values., e.g. ['externalAssayID':12]
40     * @return String url   
41     */
42    public static Object getRestResource( RestServerURL, resource, params ) {
43                def url = getRestURL( RestServerURL, resource, params )
44            def authString = "${username}:${password}".getBytes().encodeBase64().toString()
45            def conn = url.openConnection()
46            conn.setRequestProperty("Authorization", "Basic ${authString}")
47            return JSON.parse( conn.content.text )
48    }
49
50
51    /**
52     * Convenience method for constructing URLs for SAM that need parameters.
53     * Note that parameters are first convereted to strings by calling their toString() method
54     * and then Encoded to protect special characters.
55     *
56     * @params String resource The name of the resource, e.g. importer/pages
57     * @params Map params      A Map of parmater names and values., e.g. ['externalAssayID':12]
58     * @return String url   
59     */
60    public static URL getRestURL( RestServerURL, resource, params ) {
61        def url = RestServerURL + '/' + resource
62                def first = true
63                params.each { name, value ->
64                        if(first) {
65                                first = false
66                                url += '/nil?' + name + "=" + URLEncoder.encode( value.toString(), Encoding )
67                        }
68                        else {
69                                url += '&' + name + "=" + URLEncoder.encode( value.toString(), Encoding  )
70                        }
71                }
72                return new URL( url )
73    }
74
75
76
77    /**
78     * This method dynamically adds a static method to the CommunicationManager.
79     * 
80     * @params String serverURL          A rest server URL.
81     * @params String restName           The name of a rest resource on the server.     
82     * @params Map params (optional) A list of parameter names to be passed to this resource.
83     * @params Colosure (optional)   A closure that acts on the result of the Rest call in the new method.
84     * 
85     * Given a rest resource at serverURL called resourceName, we register a static method
86     * for the CommunicationManager. The new method has the same name and arity as the resource.
87     * 
88     * Example: Suppopse http://localhost:8080/gscf/rest/getSamples is a Rest resource.
89     * 
90     * In our grails app, we would like to connect to this service. We want to have a
91     * method getSamples() that fetches the result from the service. We do this by calling
92     * 
93     *          CommunicationManager.addRestWrapper( 'http://localhost:8080/gscf/rest', 'getSamples', ['externalStudyID'] )
94     * 
95     * This registers a new method:
96     * 
97         *               public static Object CommunicationManager.getSamples( Object arg )
98     * 
99     * This method has arrity 1 and expects to be given a map. The map is the parameter map
100     * of the rest service getSamples. It maps parameter called "externalStudyID" to some object
101     * that is passed. So, it can be called like as follows:
102     * 
103     *      def sampleList = CommunicationManager.getSamples( [externalStudyID:4711] )
104     * 
105     *  The call will deliver the results of the parameterized rest resource given at:
106     * 
107     *          http://localhost:8080/gscf/rest/nil?externalStudyID=4711
108     *
109     */
110
111    public static addRestWrapper( serverURL, restName, params = [], closure = { return it } ) {
112                def result
113                try {
114                        CommunicationManager.metaClass.registerStaticMethod( restName ) { Object [] strangeGroovyArgs ->
115                                def map = [:]
116                            def args = strangeGroovyArgs[0]        // groovy nests the parameters of the methods in some other array
117                                if(params.size > 0 )
118                                {
119                                        for( i in 0..(params.size-1) ) {
120                                                def param = params[i]
121                                                map[param] = args[i]
122                                        }
123                                }
124                                result = closure( getRestResource( serverURL, restName, map ) )
125                        }
126                } catch ( Exception e ) { result = RestException.getErrorObject()  }
127                return result
128        }
129
130
131
132
133
134    /**
135     * This method dynamically registers a static method to the CommunicationManager. The new method
136     * gives url for a Grails view on some server and takes as arguments the arguments required
137     * as params by the view.
138     * 
139     * @params String methodname        The name for method to be registered.
140     * @params String serverURL         The server's URL.
141     * @params String viewName          The view's name, e.g., '/Assay/show'
142     * @params Map params               The parameter list required by this view.
143     * @return String URL
144     * 
145     */ 
146    public static addViewWrapper( methodName, serverURL, viewName, params = [] ) {
147
148                CommunicationManager.metaClass.registerStaticMethod( methodName ) { Object [] strangeGroovyArgs ->
149                        def map = [:]
150                    def args = strangeGroovyArgs[0]        // groovy nests the parameters of the methods in some other array
151                        for( i in 0..(params.size-1) ) {
152                                def param = params[i]
153                            map[param] = args[i]
154                        }
155                        return getRestURL( serverURL, viewName, map )
156                }
157    }
158
159
160    /**
161     *  This creates on run time new methods for accessing Rest resources that GSCF provides for SAM.
162     *  This method should be called in grails-app/conf/BootStrap.groovy in the SAM module.
163     */
164    public static registerRestWrapperMethodsGSCFtoSAM() {
165        def url = GSCFServerURL + '/rest'
166                addRestWrapper( url , 'getStudies' )
167                addRestWrapper( url , 'getSubjects', ['studyToken'] )
168                addRestWrapper( url , 'getAssays',   ['studyToken','moduleURL'] )
169                addRestWrapper( url , 'getSamples',  ['assayToken'] )
170    }
171
172
173    /**
174     *  This method creates on run time new methods for accessing Grails views that SAM provides for GSCF.
175     *  This method should be called in grails-app/conf/BootStrap.groovy in the GSCF module.
176     */
177    public static registerRestWrapperMethodsSAMtoGSCF() {
178                def url = SAMServerURL
179
180                // register method that links to the SAM view for importing a SimpleAssay.
181        // parameters: externalAssayID, an externalAssayID
182                addViewWrapper( 'getAssayImportURL', url, 'importer/pages', ['externalAssayID', 'externalStudyID'] )
183
184                // register method that links to the SAM view for showing a SimpleAssay
185        // parameters: externalAssayID
186                addViewWrapper( 'getAssayShowURL', url, 'simpleAssay/show', ['externalAssayID'] )
187
188                // register method that links to the SAM view for editing a SimpleAssay
189        // parameters: externalAssayID
190                addViewWrapper( 'getAssayEditURL', url, 'simpleAssay/show', ['externalAssayID'] )
191
192                // register method that links to the SAM view for editing a SimpleAssay
193        // parameters: externalAssayID
194                addViewWrapper( 'getMeasurementTypesURL', url, 'simpleAssayMeasurementType/list', ['externalStudyID'] )
195
196                // register rest resource that returns the results of a full text query on SAM
197        // parameters:   query. A string for fulltext search on SAM
198        // return value: results map. It contains two keys 'studyIds', and 'assays'. 'studyIds'
199                //               key maps to a list of Study domain objects of GSCF. 'assays' map to a
200                //               list of pairs. Each pair consists of an Assay domain object of GSCF and
201                //               additional assay information from SAM provided as a map.
202                // Example of a returned map:
203                //               [studyIds:[PPSH],
204                //                               assays:[[isIntake:false, isDrug:false, correctionMethod:test Correction Method 1,
205                //                               detectableLimit:1, isNew:false, class:data.SimpleAssay, externalAssayID:1, id:1,
206                //                               measurements:null, unit:Insulin, inSerum:false, name:test Simple Assay 1,
207                //                               referenceValues:test Reference Values 1]]]
208                def closure = { map ->
209                    def studies = []   
210                    def assays  = []   
211                        def studiesHQ = "from dbnp.studycapturing.Study as s where s.code=?"
212                        map['studyIds'].each { studies.add( dbnp.studycapturing.Study.find(studiesHQ,[it]) ) }
213                        map['assays'].each { samAssay ->
214                                def assayID = samAssay['externalAssayID']
215                            def assayHQ = "from dbnp.studycapturing.Assay as a where a.externalAssayID='${assayID}'"
216                                def assay = dbnp.studycapturing.Assay.find(assayHQ)
217                                assays.add( [samAssay,assay] )
218                        }
219                        return [studies:studies, assays:assays]
220                }
221
222                addRestWrapper( url+'/rest', 'getQueryResult',  ['query'], closure )
223
224
225                // Rest resource: getQueryResultWithOperator
226                //
227                // register rest resource that returns the results of a simple query with measurement value on SAM
228        // parameters:   query. A keyword to match a measurement type on SAM.
229        //               operator. One of '=','<', or '>' that serves for selecting measurements.
230        //               value. A double value for the measurement.
231                //
232        // return value: results list of maps. each map contains an assay, a sample, the value found,
233        //               a unit (as String), and a type (as String).
234                //
235                // Example of a returned list of maps:
236                //                               [["type":"Glucose", "unit":"g", "value":"201.0", "assay":Lipid profiling,
237                //                                 "sample":A10_B], ["type":"Glucose", "unit":"g", "value":"101.0",
238                //                                 "assay":Lipid profiling, "sample":A1_B], ["type":"Insulin", "unit":"g", "value":"202.0",
239                //                                 "assay":Lipid profiling, "sample":A10_B], ["type":"Insulin", "unit":"g", "value":"102.0",
240                //                                 "assay":Lipid profiling, "sample":A1_B]]
241                //             
242                def closure2 = { listOfMaps ->
243                        def results = []
244                        listOfMaps.each{ map ->
245                                def result = [ type:map['type'], unit:map['unit'], value:map['value'] ]
246                                def assayId = map['externalAssayId'].toLong()
247                                def assay  = dbnp.studycapturing.Assay.find( "from dbnp.studycapturing.Assay as a where a.externalAssayID= ?", [assayId] )
248                                def sample = dbnp.studycapturing.Sample.find( "from dbnp.studycapturing.Sample as s where s.name = ?", [map['externalSampleId']] )
249                                result['assay']=assay
250                                result['sample']=sample
251                                results.add( result )
252                        }
253                        return results
254                }
255
256                addRestWrapper( url+'/rest', 'getQueryResultWithOperator',  ['query','operator','value'], closure2 )
257
258        }
259       
260
261
262    /**
263     * Give list of missing parameters for a parameter call in a RestController.
264     * 
265     * @params params Map params        The parameter list required by this view.
266     * @params requiredParamers                 List of parameter names that must be provided
267     * @return true, if params has all required parameters, an error hash otherwise
268     */ 
269        static String hasValidParams( params, render, Object [] requiredParams ) {
270                def list = []
271                requiredParams.each { p ->
272                        if( !params[p] ) list.push p
273                }
274                if(list.size()<=0) { return true }
275                render( ['error':list] )
276                return false  // return erro object
277        }
278
279
280}
Note: See TracBrowser for help on using the browser.