Commit | Line | Data |
532cc23b |
1 | package stemmaweb::Controller::Stemweb; |
2 | use Moose; |
3 | use namespace::autoclean; |
ed0ce314 |
4 | use Encode qw/ decode_utf8 /; |
c2b80bba |
5 | use JSON; |
70744367 |
6 | use LWP::UserAgent; |
532cc23b |
7 | use Safe::Isa; |
c55cf210 |
8 | use Scalar::Util qw/ looks_like_number /; |
532cc23b |
9 | use TryCatch; |
70744367 |
10 | use URI; |
532cc23b |
11 | |
12 | BEGIN { extends 'Catalyst::Controller' } |
13 | |
70744367 |
14 | ## TODO Move the /algorithms/available function to the Stemweb module |
15 | my $STEMWEB_BASE_URL = 'http://slinkola.users.cs.helsinki.fi'; |
16 | |
532cc23b |
17 | =head1 NAME |
18 | |
19 | stemmaweb::Controller::Stemweb - Client listener for Stemweb results |
20 | |
21 | =head1 DESCRIPTION |
22 | |
23 | This is a client listener for the Stemweb API as implemented by the protocol defined at |
24 | L<https://docs.google.com/document/d/1aNYGAo1v1WPDZi6LXZ30FJSMJwF8RQPYbOkKqHdCZEc/pub>. |
25 | |
26 | =head1 METHODS |
27 | |
28 | =head2 result |
29 | |
30 | POST stemweb/result |
31 | Content-Type: application/json |
32 | (On success): |
67b2a665 |
33 | { jobid: <ID number> |
532cc23b |
34 | status: 0 |
35 | format: <format> |
36 | result: <data> } |
37 | (On failure): |
38 | { jobid: <ID number> |
39 | status: >1 |
40 | result: <error message> } |
41 | |
42 | Used by the Stemweb server to notify us that one or more stemma graphs |
43 | has been calculated in response to an earlier request. |
44 | |
45 | =cut |
46 | |
47 | sub result :Local :Args(0) { |
48 | my( $self, $c ) = @_; |
49 | if( $c->request->method eq 'POST' ) { |
50 | # TODO: Verify the sender! |
51 | my $answer; |
52 | if( ref( $c->request->body ) eq 'File::Temp' ) { |
53 | # Read in the file and parse that. |
67b2a665 |
54 | $c->log->debug( "Request body is in a temp file" ); |
55 | open( POSTDATA, $c->request->body ) |
c0292f64 |
56 | or return _json_error( $c, 500, "Failed to open post data file" ); |
532cc23b |
57 | binmode( POSTDATA, ':utf8' ); |
58 | # JSON should be all one line |
59 | my $pdata = <POSTDATA>; |
60 | chomp $pdata; |
61 | close POSTDATA; |
c2b80bba |
62 | try { |
63 | $answer = from_json( $pdata ); |
64 | } catch { |
65 | return _json_error( $c, 400, |
66 | "Could not parse POST request '' $pdata '' as JSON: $@" ); |
67 | } |
532cc23b |
68 | } else { |
69 | $answer = from_json( $c->request->body ); |
70 | } |
67b2a665 |
71 | $c->log->debug( "Received push notification from Stemweb: " |
72 | . to_json( $answer ) ); |
c2b80bba |
73 | return _process_stemweb_result( $c, $answer ); |
74 | } else { |
75 | return _json_error( $c, 403, 'Please use POST!' ); |
76 | } |
77 | } |
78 | |
66458003 |
79 | =head2 available |
80 | |
81 | GET algorithms/available |
82 | |
83 | Queries the Stemweb server for available stemma generation algorithms and their |
84 | parameters. Returns the JSON answer as obtained from Stemweb. |
85 | |
86 | =cut |
87 | |
88 | sub available :Local :Args(0) { |
89 | my( $self, $c ) = @_; |
90 | my $ua = LWP::UserAgent->new(); |
91 | my $resp = $ua->get( $STEMWEB_BASE_URL . '/algorithms/available' ); |
92 | if( $resp->is_success ) { |
6aabefa3 |
93 | $c->stash->{'result'} = decode_json( $resp->content ); |
66458003 |
94 | } else { |
6aabefa3 |
95 | $c->stash->{'result'} = {}; |
66458003 |
96 | } |
97 | $c->forward('View::JSON'); |
98 | } |
99 | |
c2b80bba |
100 | =head2 query |
101 | |
102 | GET stemweb/query/<jobid> |
103 | |
104 | A backup method to query the stemweb server to check a particular job status. |
105 | Returns a result as in /stemweb/result above, but status can also be -1 to |
106 | indicate that the job is still running. |
107 | |
108 | =cut |
109 | |
110 | sub query :Local :Args(1) { |
111 | my( $self, $c, $jobid ) = @_; |
112 | my $ua = LWP::UserAgent->new(); |
2c514a6f |
113 | my $resp = $ua->get( $STEMWEB_BASE_URL . "/algorithms/jobstatus/$jobid" ); |
c2b80bba |
114 | if( $resp->is_success ) { |
115 | # Process it |
116 | my $response = decode_utf8( $resp->content ); |
117 | $c->log->debug( "Got a response from the server: $response" ); |
118 | my $answer; |
119 | try { |
120 | $answer = from_json( $response ); |
121 | } catch { |
532cc23b |
122 | return _json_error( $c, 500, |
c2b80bba |
123 | "Could not parse stemweb response '' $response '' as JSON: $@" ); |
124 | } |
125 | return _process_stemweb_result( $c, $answer ); |
126 | } elsif( $resp->code == 500 && $resp->header('Client-Warning') |
127 | && $resp->header('Client-Warning') eq 'Internal response' ) { |
128 | # The server was unavailable. |
129 | return _json_error( $c, 503, "The Stemweb server is currently unreachable." ); |
130 | } else { |
131 | return _json_error( $c, 500, "Stemweb error: " . $resp->code . " / " |
132 | . $resp->content ); |
133 | } |
134 | } |
135 | |
136 | |
137 | ## Helper function for parsing Stemweb result data either by push or by pull |
138 | sub _process_stemweb_result { |
139 | my( $c, $answer ) = @_; |
c0292f64 |
140 | # Find the specified tradition and check its job ID. |
c2b80bba |
141 | my $m = $c->model('Directory'); |
c0292f64 |
142 | my $tradition = $m->tradition( $answer->{textid} ); |
143 | unless( $tradition ) { |
144 | return _json_error( $c, 400, "No tradition found with ID " |
145 | . $answer->{textid} ); |
146 | } |
147 | if( $answer->{status} == 0 ) { |
148 | my $stemmata; |
149 | if( $tradition->has_stemweb_jobid |
150 | && $tradition->stemweb_jobid eq $answer->{jobid} ) { |
c2b80bba |
151 | try { |
152 | $stemmata = $tradition->record_stemweb_result( $answer ); |
153 | $m->save( $tradition ); |
154 | } catch( Text::Tradition::Error $e ) { |
155 | return _json_error( $c, 500, $e->message ); |
156 | } catch { |
157 | return _json_error( $c, 500, $@ ); |
158 | } |
c0292f64 |
159 | } else { |
160 | # It may be that we already received a callback meanwhile. |
161 | # Check all stemmata for the given jobid and return them. |
162 | @$stemmata = grep { $_->came_from_jobid && $_->from_jobid eq $answer->{jobid} } $tradition->stemmata; |
163 | } |
c0292f64 |
164 | if( @$stemmata ) { |
c2b80bba |
165 | # If we got here, success! |
c55cf210 |
166 | # TODO Use helper in Root.pm to do this |
c2b80bba |
167 | my @steminfo = map { { |
168 | name => $_->identifier, |
169 | directed => _json_bool( !$_->is_undirected ), |
170 | svg => $_->as_svg() } } |
2c514a6f |
171 | @$stemmata; |
c2b80bba |
172 | $c->stash->{'result'} = { |
173 | 'status' => 'success', |
174 | 'stemmata' => \@steminfo }; |
c2b80bba |
175 | } else { |
c0292f64 |
176 | # Hm, no stemmata found on this tradition with this jobid. |
177 | # Clear the tradition jobid so that the user can try again. |
178 | if( $tradition->has_stemweb_jobid ) { |
179 | $tradition->_clear_stemweb_jobid; |
180 | $m->save( $tradition ); |
181 | } |
182 | $c->stash->{'result'} = { status => 'notfound' }; |
c2b80bba |
183 | } |
c0292f64 |
184 | } elsif( $answer->{status} < 1 ) { |
185 | $c->stash->{'result'} = { 'status' => 'running' }; |
c2b80bba |
186 | } else { |
c0292f64 |
187 | return _json_error( $c, 500, |
188 | "Stemweb failure not handled: " . $answer->{result} ); |
532cc23b |
189 | } |
c2b80bba |
190 | $c->forward('View::JSON'); |
532cc23b |
191 | } |
192 | |
70744367 |
193 | =head2 request |
194 | |
195 | GET stemweb/request/? |
196 | tradition=<tradition ID> & |
197 | algorithm=<algorithm ID> & |
198 | [<algorithm parameters>] |
199 | |
200 | Send a request for the given tradition with the given parameters to Stemweb. |
201 | Processes and returns the JSON response given by the Stemweb server. |
202 | |
203 | =cut |
204 | |
205 | sub request :Local :Args(0) { |
206 | my( $self, $c ) = @_; |
207 | # Look up the relevant tradition and check permissions. |
208 | my $reqparams = $c->req->params; |
209 | my $tid = delete $reqparams->{tradition}; |
210 | my $t = $c->model('Directory')->tradition( $tid ); |
211 | my $ok = _check_permission( $c, $t ); |
212 | return unless $ok; |
213 | return( _json_error( $c, 403, |
214 | 'You do not have permission to update stemmata for this tradition' ) ) |
215 | unless $ok eq 'full'; |
216 | |
217 | # Form the request for Stemweb. |
218 | my $algorithm = delete $reqparams->{algorithm}; |
219 | my $return_uri = URI->new( $c->uri_for( '/stemweb/result' ) ); |
220 | my $stemweb_request = { |
221 | return_path => $return_uri->path, |
222 | return_host => $return_uri->host_port, |
67b2a665 |
223 | data => $t->collation->as_tsv({noac => 1}), |
c2b80bba |
224 | userid => $c->user->get_object->email, |
67b2a665 |
225 | textid => $tid, |
c55cf210 |
226 | parameters => _cast_nonstrings( $reqparams ) }; |
70744367 |
227 | |
228 | # Call to the appropriate URL with the request parameters. |
229 | my $ua = LWP::UserAgent->new(); |
c2b80bba |
230 | $c->log->debug( 'Sending request to Stemweb: ' . to_json( $stemweb_request ) ); |
70744367 |
231 | my $resp = $ua->post( $STEMWEB_BASE_URL . "/algorithms/process/$algorithm/", |
232 | 'Content-Type' => 'application/json; charset=utf-8', |
233 | 'Content' => encode_json( $stemweb_request ) ); |
234 | if( $resp->is_success ) { |
235 | # Process it |
ed0ce314 |
236 | $c->log->debug( 'Got a response from the server: ' |
c2b80bba |
237 | . decode_utf8( $resp->content ) ); |
70744367 |
238 | my $stemweb_response = decode_json( $resp->content ); |
239 | try { |
240 | $t->set_stemweb_jobid( $stemweb_response->{jobid} ); |
241 | } catch( Text::Tradition::Error $e ) { |
242 | return _json_error( $c, 429, $e->message ); |
243 | } |
244 | $c->model('Directory')->save( $t ); |
245 | $c->stash->{'result'} = $stemweb_response; |
246 | $c->forward('View::JSON'); |
247 | } elsif( $resp->code == 500 && $resp->header('Client-Warning') |
248 | && $resp->header('Client-Warning') eq 'Internal response' ) { |
249 | # The server was unavailable. |
250 | return _json_error( $c, 503, "The Stemweb server is currently unreachable." ); |
251 | } else { |
ed0ce314 |
252 | return _json_error( $c, 500, "Stemweb error: " . $resp->code . " / " |
70744367 |
253 | . $resp->content ); |
254 | } |
255 | } |
256 | |
257 | # Helper to check what permission, if any, the active user has for |
258 | # the given tradition |
259 | sub _check_permission { |
260 | my( $c, $tradition ) = @_; |
261 | my $user = $c->user_exists ? $c->user->get_object : undef; |
262 | if( $user ) { |
263 | return 'full' if ( $user->is_admin || |
264 | ( $tradition->has_user && $tradition->user->id eq $user->id ) ); |
265 | } |
266 | # Text doesn't belong to us, so maybe it's public? |
267 | return 'readonly' if $tradition->public; |
268 | |
269 | # ...nope. Forbidden! |
270 | return _json_error( $c, 403, 'You do not have permission to view this tradition.' ); |
271 | } |
272 | |
c55cf210 |
273 | # QUICK HACK to deal with strict Stemweb validation. |
274 | sub _cast_nonstrings { |
275 | my $params = shift; |
276 | foreach my $k ( keys %$params ) { |
277 | my $v = $params->{$k}; |
278 | if( looks_like_number( $v ) ) { |
279 | $params->{$k} = $v * 1; |
280 | } elsif ( !defined $v || $v eq 'true' ) { |
281 | $params->{$k} = _json_bool( $v ); |
282 | } |
283 | } |
284 | return $params; |
285 | } |
286 | |
532cc23b |
287 | # Helper to throw a JSON exception |
288 | sub _json_error { |
289 | my( $c, $code, $errmsg ) = @_; |
290 | $c->response->status( $code ); |
291 | $c->stash->{'result'} = { 'error' => $errmsg }; |
292 | $c->forward('View::JSON'); |
293 | return 0; |
c55cf210 |
294 | } |
532cc23b |
295 | |
c2b80bba |
296 | sub _json_bool { |
297 | return $_[0] ? JSON::true : JSON::false; |
298 | } |
299 | |
300 | |
ed0ce314 |
301 | 1; |