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