Skip to content

Commit 02618ef

Browse files
committed
Merge: app: portable services to do requests over HTTP asynchronously
This PR introduces a series of portable services to execute HTTP request asynchronously from graphical programs. These services should be independent and may be reorganized as needed by client programs. The HTTP request services are very simple by design, it is not an attempt to define a true API to build the request. It is currently limited to GET calls to a simple URI, for example a simple use may look like: ~~~ print "http://xymus.net/rest/list?query=asdf".http_get.value ~~~ ## The services * `Text::http_get` makes an HTTP request and blocks until the response is received. It returns `HttpRequestResult`, a subclass of `MaybeError`, with a possible error, status code and response body content. This service is implemented independently on each platform, using GDK + Curl on GNU/Linux and Apache HTTP client services in Java on Android. * `App::run_on_ui_thread` sends an instance of `Task` to be executed on the main UI thread when possible. This method may be moved "up" to `app::ui` as needed. * `AsyncHttpRequest` combines the two previous features to execute an HTTP request asynchronously, deserialize the result from JSON (if needed) and execute custom behaviors on the main UI thread. Users of this service should subclass `AsyncHttpRequest` and implement as needed `before`, `on_load`, `on_error` and `after`. By default, all user code is executed on the main UI thread and as such users do not have to worry about threading logic. Pull-Request: #1823 Reviewed-by: Jean Privat <jean@pryen.org>
2 parents fde422c + 805bcb6 commit 02618ef

File tree

7 files changed

+378
-13
lines changed

7 files changed

+378
-13
lines changed

lib/android/http_request.nit

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# This file is part of NIT ( http://www.nitlanguage.org ).
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Android implementation of `app:http_request`
16+
module http_request is
17+
android_manifest """<uses-permission android:name="android.permission.INTERNET" />"""
18+
end
19+
20+
intrude import app::http_request
21+
import ui
22+
23+
in "Java" `{
24+
import org.apache.http.client.methods.HttpGet;
25+
import org.apache.http.impl.client.DefaultHttpClient;
26+
import org.apache.http.HttpResponse;
27+
import org.apache.http.HttpStatus;
28+
import org.apache.http.StatusLine;
29+
import java.io.ByteArrayOutputStream;
30+
`}
31+
32+
redef class App
33+
redef fun run_on_ui_thread(task) do app.native_activity.run_on_ui_thread task
34+
end
35+
36+
redef class Text
37+
38+
redef fun http_get
39+
do
40+
jni_env.push_local_frame 8
41+
var juri = self.to_java_string
42+
var jrep = java_http_get(juri)
43+
44+
assert not jrep.is_java_null
45+
46+
var res
47+
if jrep.is_exception then
48+
jrep = jrep.as_exception
49+
res = new HttpRequestResult(null, new IOError(jrep.message.to_s))
50+
else if jrep.is_http_response then
51+
jrep = jrep.as_http_response
52+
res = new HttpRequestResult(jrep.content.to_s, null, jrep.status_code)
53+
else abort
54+
55+
jni_env.pop_local_frame
56+
return res
57+
end
58+
end
59+
60+
redef class AsyncHttpRequest
61+
62+
redef fun main
63+
do
64+
var res = super
65+
jvm.detach_current_thread
66+
return res
67+
end
68+
end
69+
70+
redef class JavaObject
71+
private fun is_exception: Bool in "Java" `{ return self instanceof Exception; `}
72+
private fun as_exception: JavaException in "Java" `{ return (Exception)self; `}
73+
74+
private fun is_http_response: Bool in "Java" `{ return self instanceof HttpResponse; `}
75+
private fun as_http_response: JavaHttpResponse in "Java" `{ return (HttpResponse)self; `}
76+
end
77+
78+
private fun java_http_get(uri: JavaString): JavaObject in "Java" `{
79+
try {
80+
DefaultHttpClient client = new DefaultHttpClient();
81+
HttpGet get = new HttpGet(uri);
82+
return client.execute(get);
83+
} catch (Exception ex) {
84+
return ex;
85+
}
86+
`}
87+
88+
private extern class JavaHttpResponse in "Java" `{ org.apache.http.HttpResponse `}
89+
super JavaObject
90+
91+
fun status_code: Int in "Java" `{ return self.getStatusLine().getStatusCode(); `}
92+
93+
fun content: JavaString in "Java" `{
94+
try {
95+
ByteArrayOutputStream out = new ByteArrayOutputStream();
96+
self.getEntity().writeTo(out);
97+
out.close();
98+
return out.toString();
99+
} catch (Exception ex) {
100+
ex.printStackTrace();
101+
return "";
102+
}
103+
`}
104+
end

lib/app/http_request.nit

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# This file is part of NIT ( http://www.nitlanguage.org ).
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# HTTP request services: `AsyncHttpRequest` and `Text::http_get`
16+
module http_request
17+
18+
import app_base
19+
import pthreads
20+
import json::serialization
21+
22+
import linux::http_request is conditional(linux)
23+
import android::http_request is conditional(android)
24+
25+
redef class App
26+
# Platform specific service to execute `task` on the main/UI thread
27+
fun run_on_ui_thread(task: Task) is abstract
28+
end
29+
30+
# Thread executing an HTTP request and deserializing JSON asynchronously
31+
#
32+
# This class defines four methods acting on the main/UI thread,
33+
# they should be implemented as needed:
34+
# * before
35+
# * on_load
36+
# * on_fail
37+
# * after
38+
class AsyncHttpRequest
39+
super Thread
40+
41+
# Root URI of the remote server
42+
fun rest_server_uri: String is abstract
43+
44+
# Action, or path, for this request within the `rest_server_uri`
45+
fun rest_action: String is abstract
46+
47+
# Should the response content be deserialized from JSON?
48+
var deserialize_json = true is writable
49+
50+
redef fun start
51+
do
52+
before
53+
super
54+
end
55+
56+
redef fun main
57+
do
58+
var uri = rest_server_uri / rest_action
59+
60+
# Execute REST request
61+
var rep = uri.http_get
62+
if rep.is_error then
63+
app.run_on_ui_thread new RestRunnableOnFail(self, rep.error)
64+
return null
65+
end
66+
67+
if not deserialize_json then
68+
app.run_on_ui_thread new RestRunnableOnLoad(self, rep)
69+
return null
70+
end
71+
72+
# Deserialize
73+
var deserializer = new JsonDeserializer(rep.value)
74+
var res = deserializer.deserialize
75+
if deserializer.errors.not_empty then
76+
app.run_on_ui_thread new RestRunnableOnFail(self, deserializer.errors.first)
77+
end
78+
79+
app.run_on_ui_thread new RestRunnableOnLoad(self, res)
80+
return null
81+
end
82+
83+
# Prepare the UI or other parts of the program before executing the REST request
84+
fun before do end
85+
86+
# Invoked when the HTTP request returned valid data
87+
#
88+
# If `deserialize_json`, the default behavior, this method is invoked only if deserialization was successful.
89+
# In this case, `result` may be any deserialized object.
90+
#
91+
# Otherwise, if `not deserialize_json`, `result` contains the content of the response as a `String`.
92+
fun on_load(result: nullable Object) do end
93+
94+
# Invoked when the HTTP request has failed and no data was received or deserialization failed
95+
fun on_fail(error: Error) do print_error "REST request '{rest_action}' failed with: {error}"
96+
97+
# Complete this request whether it was a success or not
98+
fun after do end
99+
end
100+
101+
redef class Text
102+
# Execute an HTTP GET request synchronously at the URI `self`
103+
#
104+
# ~~~nitish
105+
# var response = "http://example.org/".http_get
106+
# if response.is_error then
107+
# print_error response.error
108+
# else
109+
# print "HTTP status code: {response.code}"
110+
# print response.value
111+
# end
112+
# ~~~
113+
private fun http_get: HttpRequestResult is abstract
114+
end
115+
116+
# Result of a call to `Text::http_get`
117+
#
118+
# Users should first check if `is_error` to use `error`.
119+
# Otherwise they can use `value` to get the content of the response
120+
# and `code` for the HTTP status code.
121+
class HttpRequestResult
122+
super MaybeError[String, Error]
123+
124+
# The HTTP status code, if any
125+
var maybe_code: nullable Int
126+
127+
# The status code
128+
# Require: `not is_error`
129+
fun code: Int do return maybe_code.as(not null)
130+
end
131+
132+
private abstract class HttpRequestTask
133+
super Task
134+
135+
# `AsyncHttpRequest` to which send callbacks
136+
var sender_thread: AsyncHttpRequest
137+
end
138+
139+
private class RestRunnableOnLoad
140+
super HttpRequestTask
141+
142+
var res: nullable Object
143+
144+
redef fun main
145+
do
146+
sender_thread.on_load(res)
147+
sender_thread.after
148+
end
149+
end
150+
151+
private class RestRunnableOnFail
152+
super HttpRequestTask
153+
154+
var error: Error
155+
156+
redef fun main
157+
do
158+
sender_thread.on_fail(error)
159+
sender_thread.after
160+
end
161+
end

lib/core/error.nit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class MaybeError[V, E: Error]
7979
# REQUIRE: `not is_error`
8080
fun value: V do return maybe_value.as(V)
8181

82-
# The require
82+
# The error
8383
# REQUIRE: `is_error`
8484
fun error: E do return maybe_error.as(E)
8585

lib/gtk/v3_4/gdk.nit

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,29 @@ module gdk is pkgconfig "gtk+-3.0"
1818
import gtk_core
1919

2020
`{
21-
#ifdef GdkCallback_run
22-
// Callback to GdkCallaback::run
23-
gboolean nit_gdk_callback(gpointer user_data) {
24-
GdkCallback_decr_ref(user_data);
25-
return GdkCallback_run(user_data);
21+
#ifdef Task_gdk_main
22+
// Callback to Task::gdk_main
23+
gboolean nit_gdk_callback_task(gpointer user_data) {
24+
Task_decr_ref(user_data);
25+
return Task_gdk_main(user_data);
2626
}
2727
#endif
2828
`}
2929

30-
# Callback to pass to `gdk_threads_add_idle`
31-
class GdkCallback
30+
redef class Task
3231

3332
# Small unit of code executed by the GDK loop when idle
3433
#
35-
# Returns true if this object should be invoked again.
36-
fun run: Bool do return false
34+
# Returns `true` if this object should be invoked again.
35+
fun gdk_main: Bool
36+
do
37+
main
38+
return false
39+
end
3740
end
3841

3942
# Add a callback to execute whenever there are no higher priority events pending
40-
fun gdk_threads_add_idle(callback: GdkCallback): Int import GdkCallback.run `{
41-
GdkCallback_incr_ref(callback);
42-
return gdk_threads_add_idle(&nit_gdk_callback, callback);
43+
fun gdk_threads_add_idle(task: Task): Int import Task.gdk_main `{
44+
Task_incr_ref(task);
45+
return gdk_threads_add_idle(&nit_gdk_callback_task, task);
4346
`}

lib/java/base.nit

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,53 @@ redef extern class JavaObject
193193
return to_java_string.to_s
194194
end
195195
end
196+
197+
# Java class: java.lang.Throwable
198+
extern class JavaThrowable in "Java" `{ java.lang.Throwable `}
199+
super JavaObject
200+
201+
# Java implementation: java.lang.String java.lang.Throwable.getMessage()
202+
fun message: JavaString in "Java" `{
203+
return self.getMessage();
204+
`}
205+
206+
# Java implementation: java.lang.String java.lang.Throwable.getLocalizedMessage()
207+
fun localized_message: JavaString in "Java" `{
208+
return self.getLocalizedMessage();
209+
`}
210+
211+
# Java implementation: java.lang.Throwable.printStackTrace()
212+
fun print_stack_trace in "Java" `{
213+
self.printStackTrace();
214+
`}
215+
216+
# Java implementation: java.lang.Throwable java.lang.Throwable.getCause()
217+
fun cause: JavaThrowable in "Java" `{
218+
return self.getCause();
219+
`}
220+
221+
redef fun new_global_ref import sys, Sys.jni_env `{
222+
Sys sys = JavaThrowable_sys(self);
223+
JNIEnv *env = Sys_jni_env(sys);
224+
return (*env)->NewGlobalRef(env, self);
225+
`}
226+
227+
redef fun pop_from_local_frame_with_env(jni_env) `{
228+
return (*jni_env)->PopLocalFrame(jni_env, self);
229+
`}
230+
end
231+
232+
# Java class: java.lang.Exception
233+
extern class JavaException in "Java" `{ java.lang.Exception `}
234+
super JavaThrowable
235+
236+
redef fun new_global_ref import sys, Sys.jni_env `{
237+
Sys sys = JavaException_sys(self);
238+
JNIEnv *env = Sys_jni_env(sys);
239+
return (*env)->NewGlobalRef(env, self);
240+
`}
241+
242+
redef fun pop_from_local_frame_with_env(jni_env) `{
243+
return (*jni_env)->PopLocalFrame(jni_env, self);
244+
`}
245+
end

lib/jvm.nit

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,14 @@ extern class JavaVM `{JavaVM *`}
183183
}
184184
return env;
185185
`}
186+
187+
# Detach the calling thread from this JVM
188+
fun detach_current_thread import jni_error `{
189+
int res = (*self)->DetachCurrentThread(self);
190+
if (res != JNI_OK) {
191+
JavaVM_jni_error(NULL, "Could not detach current thread to Java VM", res);
192+
}
193+
`}
186194
end
187195

188196
# Represents a jni JNIEnv, which is a thread in a JavaVM

0 commit comments

Comments
 (0)