File Coverage

lib/MojoX/Dispatcher/Qooxdoo/Jsonrpc.pm
Criterion Covered Total %
statement 88 108 81.4
branch 28 36 77.7
condition 7 15 46.6
subroutine 8 9 88.8
pod 0 1 0.0
total 131 169 77.5


line stmt bran cond sub pod time code
1             package MojoX::Dispatcher::Qooxdoo::Jsonrpc;
2              
3 1     1   879 use strict;
  1         1  
  1         34  
4 1     1   5 use warnings;
  1         1  
  1         29  
5              
6 1     1   14 use Mojo::JSON;
  1         2  
  1         54  
7 1     1   4 use Mojo::Base 'Mojolicious::Controller';
  1         1  
  1         9  
8 1     1   147432 use Encode;
  1         3  
  1         121  
9 1     1   7 use Data::Dumper;
  1         2  
  1         1016  
10              
11             our $toUTF8 = find_encoding('utf8');
12              
13             our $VERSION = '0.95';
14              
15             has 'JSON' => sub { Mojo::JSON->new };
16              
17             sub dispatch {
18 9     9 0 208564 my $self = shift;
19            
20             # We have to differentiate between POST and GET requests, because
21             # the data is not sent in the same place..
22 9         202 my $log = $self->app->log;
23              
24 9         446 my $json = $self->JSON;
25              
26             # send warnings to log file preserving the origin
27             local $SIG{__WARN__} = sub {
28 0     0   0 my $message = shift;
29 0         0 $message =~ s/\n$//;
30 0         0 @_ = ($log, $message);
31 0         0 goto &Mojo::Log::warn;
32 9         150 };
33 9         22 my $id;
34             my $data;
35 0         0 my $cross_domain;
36 9         38 for ( $self->req->method ){
37 9 100       655 /^POST$/ && do {
38             # Data comes as JSON object, so fetch a reference to it
39             $data = $json->decode($self->req->body) or
40 8 100       27 do {
41 1         2609 my $error = "Invalid json string: " . $json->error;
42 1         13 $log->error($error);
43 1         33 $self->render(text => $error, status=>500);
44 1         1187 return;
45             };
46 7         1921 $id = $data->{id};
47 7         12 $cross_domain = 0;
48 7         19 next;
49             };
50 1 50       10 /^GET$/ && do {
51 1         13 my $v = $self->param('_ScriptTransport_data');
52             $data= $json->decode(
53             $self->param('_ScriptTransport_data')
54             ) or
55 1 50       542 do {
56 0         0 my $error = "Invalid json string: " . $json->error . " " .Dumper $self->param;
57 0         0 $log->error($error);
58 0         0 $self->render(text => $error, status=>500);
59 0         0 return;
60             };
61              
62 1         364 $id = $self->param('_ScriptTransport_id') ;
63 1         106 $cross_domain = 1;
64 1         4 next;
65             };
66 0         0 my $error = "request must be POST or GET. Can't handle '".$self->req->method."'";
67 0         0 $log->error($error);
68 0         0 $self->render(text => $error, status=>500);
69 0         0 return;
70             }
71 8 100       27 if (not defined $id){
72 1         4 my $error = "Missing 'id' property in JsonRPC request.";
73 1         6 $log->error($error);
74 1         25 $self->render(text => $error, status=>500);
75 1         1021 return;
76             }
77              
78              
79             # Check if desired service is available
80 7 100       23 my $service = $data->{service} or do {
81 1         3 my $error = "Missing service property in JsonRPC request.";
82 1         6 $log->error($error);
83 1         26 $self->render(text => $error, status=>500);
84 1         1057 return;
85             };
86              
87             # Check if method is not private (marked with a leading underscore)
88 6 100       30 my $method = $data->{method} or do {
89 1         3 my $error = "Missing method property in JsonRPC request.";
90 1         6 $log->error($error);
91 1         44 $self->render(text => $error, status=>500);
92 1         983 return;
93             };
94            
95 5   100     28 my $params = $data->{params} || []; # is a reference, so "unpack" it
96            
97             # invocation of method in class according to request
98 5         9 my $reply = eval{
99             # make sure there are not foreign signal handlers
100             # messing with our problems
101 5         32 local $SIG{__DIE__};
102             # Getting available services from stash
103 5         20 my $svc = $self->stash('services')->{$service};
104              
105 5 100       85 die {
106             origin => 1,
107             message => "service $service not available",
108             code=> 2
109             } if not ref $svc;
110              
111 4 50       31 die {
112             origin => 1,
113             message => "your rpc service object (".ref($svc).") must provide an allow_rpc_access method",
114             code=> 2
115             } unless $svc->can('allow_rpc_access');
116              
117            
118 4 50       20 if ($svc->can('controller')){
119             # initialize session if it does not exists yet
120 0         0 $svc->controller($self);
121             }
122              
123 4 50       18 if ($svc->can('mojo_session')){
124             # initialize session if it does not exists yet
125 0         0 $log->warn('mojo_session is deprecated. Use controller->session instead');
126 0   0     0 my $session = $self->stash->{'mojo.session'} ||= {};
127 0         0 $svc->mojo_session($session);
128             }
129              
130 4 50       22 if ($svc->can('mojo_stash')){
131 0         0 $log->warn('mojo_stash is deprecated. Use controller->stash instead');
132             # initialize session if it does not exists yet
133 0         0 $svc->mojo_stash($self->stash);
134             }
135              
136             die {
137 4 100       22 origin => 1,
138             message => "rpc access to method $method denied",
139             code=> 6
140             } unless $svc->allow_rpc_access($method);
141              
142 3 50       15 die {
143             origin => 1,
144             message => "method $method does not exist.",
145             code=> 4
146             } if not $svc->can($method);
147              
148 3         15 $log->debug("call $method(".$json->encode($params).")");
149             # reply
150 1     1   6 no strict 'refs';
  1         1  
  1         482  
151 3         183 $svc->$method(@$params);
152             };
153            
154 5 100       89 if ($@){
155 3         26 my $error;
156 3         13 for (ref $@){
157 3 100 66     25 /HASH/ && $@->{message} && do {
158 2   50     20 $error = {
159             origin => $@->{origin} || 2,
160             message => $@->{message},
161             code=>$@->{code}
162             };
163 2         6 last;
164             };
165 1 50 33     19 /.+/ && $@->can('message') && $@->can('code') && do {
      33        
166 1         19 $error = {
167             origin => 2,
168             message => $@->message(),
169             code=>$@->code()
170             };
171 1         25 last;
172             };
173 0         0 $error = {
174             origin => 2,
175             message => "error while processing ${service}::$method: $@",
176             code=> 9999
177             };
178             }
179 3         22 $reply = $json->encode({ id => $id, error => $error });
180 3         448 $log->error("JsonRPC Error $error->{code}: $error->{message}");
181             }
182             else {
183 2         13 $reply = $json->encode({ id => $id, result => $reply });
184 2         94 $log->debug("return ".$reply);
185             }
186              
187 5 100       100 if ($cross_domain){
188             # for GET requests, qooxdoo expects us to send a javascript method
189             # and to wrap our json a litte bit more
190 1         4 $self->res->headers->content_type('application/javascript; charset=utf-8');
191 1         107 $reply = "qx.io.remote.transport.Script._requestFinished( $id, " . $reply . ");";
192             } else {
193 4         17 $self->res->headers->content_type('application/json; charset=utf-8');
194             }
195             # the render takes care of encoding the output, so make sure we re-decode
196             # the json stuf
197 5         417 $self->render(text => $toUTF8->decode($reply));
198             }
199              
200             1;
201              
202              
203              
204             =head1 NAME
205              
206             MojoX::Dispatcher::Qooxdoo::Jsonrpc - Dispatcher for Qooxdoo Json Rpc Calls
207              
208             =head1 SYNOPSIS
209              
210             # lib/your-application.pm
211              
212             use base 'Mojolicious';
213            
214             use RpcService;
215              
216             sub startup {
217             my $self = shift;
218            
219             # instantiate all services
220             my $services= {
221             Test => RpcService->new(),
222            
223             };
224            
225            
226             # add a route to the Qooxdoo dispatcher and route to it
227             my $r = $self->routes;
228             $r->route('/qooxdoo') -> to(
229             'Jsonrpc#dispatch',
230             services => $services,
231             debug => 0,
232             namespace => 'MojoX::Dispatcher::Qooxdoo'
233             );
234            
235             }
236              
237            
238              
239             =head1 DESCRIPTION
240              
241             L dispatches incoming
242             rpc requests from a qooxdoo application to your services and renders
243             a (hopefully) valid json reply.
244              
245              
246             =head1 EXAMPLE
247              
248             This example exposes a service named "Test" in a folder "RpcService".
249             The Mojo application is named "QooxdooServer". The scripts are in
250             the 'example' directory.
251             First create this application using
252             "mojolicious generate app QooxdooServer".
253              
254             Then, lets write the service:
255              
256             Change to the root directory "qooxdoo_server" of your fresh
257             Mojo-Application and make a dir named 'qooxdoo-services'
258             for the services you want to expose.
259              
260             Our "Test"-service could look like:
261              
262             package RpcService;
263              
264             use Mojo::Base -base;
265              
266             # if you want to access mojo specific information
267             # provide a controller property, it will be set to the
268             # current controller as the request is dispached.
269             # see L for documentation.
270             has 'controller';
271            
272             # MANDADROY access check method. The method is called right before the actual
273             # method call, after assigning mojo_session and mojo_stash properties are set.
274             # These can be used for providing dynamic access control
275              
276             our %access = (
277             add => 1,
278             );
279              
280             sub allow_rpc_access {
281             my $self = shift;
282             my $method = shift;
283             # check if we can access
284             return $access{$method};
285             }
286              
287             sub add{
288             my $self = shift;
289             my @params = @_;
290            
291             # Debug message on Mojo-server console (or log)
292             print "Debug: $params[0] + $params[1]\n";
293            
294             # uncomment if you want to die without further handling
295             # die;
296            
297             # uncomment if you want to die with a message in a hash
298             # die {code => 20, message => "Test died on purpose :-)"};
299            
300            
301             # uncomment if you want to die with your homemade error object
302             # die MyException->new(code=>123,message=>'stupid error message');
303            
304             my $result = $params[0] + $params[1]
305             return $result;
306             }
307              
308             package MyException;
309             use Mojo::Base -base;
310             has 'code';
311             has 'message';
312             1;
313              
314             The Dispatcher executes all calls to your service module within an eval
315             wrapper and will send any execptions you generate within back to the
316             qooxdoo application as well as into the Mojolicious logfile.
317              
318             Now, lets write our application. Normally one would use the services of
319             L for this. If you want to use the
320             dipatcher directly, this is how it is done.
321              
322             package QooxdooServer;
323              
324             use strict;
325             use warnings;
326            
327             use RpcService::Test;
328              
329             use Mojo::Base 'Mojolicious';
330              
331             # This method will run once at server start
332             sub startup {
333             my $self = shift;
334            
335             my $services= {
336             Test => RpcService::Test->new(),
337             # more services here
338             };
339            
340             # tell Mojo about your services:
341             my $r = $self->routes;
342            
343             # this sends all requests for "/qooxdoo" in your Mojo server
344             # to our little dispatcher.
345             # change this at your own taste.
346             $r->route('/qooxdoo')->to('
347             jsonrpc#dispatch',
348             services => $services,
349             namespace => 'MojoX::Dispatcher::Qooxdoo'
350             );
351            
352             }
353              
354             1;
355              
356             Now start your Mojo Server by issuing C