Опыт применения http запросов в реальном j2me приложении

Около 2-х лет назад решил попробовать себя в разработке приложений для телефонов. А именно — на j2me.
Практически во всех задачах требовалось обеспечить обмен информацией между мобильным приложением и сервером по http. Написав пару — тройку приложений, решил оформить код работы с соединением в виде отдельных классов, которые использую в своих текущих проектах.
Итак, при программировании обмена информацией между мобильным приложением и сервером каждому программисту приходится решать несколько задач
1. Создание потока соединения и обмен данными между интерфейсом пользователя и этим потоком.
2. Обеспечение реакции потока на обрыв связи или действия пользователя — например отмену соединения
3. Парсинг ответа и обновление интерфейса в зависимости от содержимого ответа.

Ниже приведен код с комментариями. Думаю он будет полезен для начинающих проограммистов, поскольку приходилось видеть довольно кривые примеры реализации описанного функционала. Хотя, не могу сказать, что меня самого до конца устраивает то, что я написал. Но по крайней мере данный код был неоднократно проверен на практике.

Замечание: в коде используются ссылки на классы из библиотеки LWUIT.

Итак поехали.
Класс Sender. Создает соединение, посылает запрос и возвращает ответ
Интерфейс ResponseHandler — занимается парсингом ответа
Класс SimpleResponseHandler — реализация интерфейса ResponseHandler
Интерфейс ConnectedListener — сигнализирует об успешном выполнении запроса
Класс WatchSheduler — реализует ConnectedListener. Представляет собой шедулер, который стартует с заданной задержкой и проверяет был ли сигнал от соединения. Если сигнала не было, убивает поток соединения.
Интерфейс AbortConnection — содержит методы для получения и остановки потока.
Класс AbortConnectionAdapter реализует AbortConnection а также обеспечивает показ сплэш-формы на время соединения и реакцию на отмену соединения пользователем.

Как это все работает.
Сначала пропишем интерфейсы

public interface AbortConnection {
public void cancel();
public void showSplashForm();
public void setAbortedThread(Thread t);
public Thread getAbortedThread();
}


public interface ConnectListener {

public void connected();
public void start();
}


public interface ResponseHandler {
void handleResponse(byte[] response); 
}


Реализация интерфейсов

public class AbortConnectionAdapter implements AbortConnection{
Thread abortedThread;

public void cancel() {
abortedThread.interrupt();
}

public void showSplashForm() {}

public void setAbortedThread(Thread t) {
abortedThread= t;
}

public Thread getAbortedThread() {
return abortedThread;
}
}


abstract class SimpleResponseHandler implements ResponseHandler {
UIContext context;
SimpleResponseHandler(UIContext context){
this.context= context;
}
public void handleResponse(byte[] response) {
final String s= new String(response);
new Thread(new Runnable(){

public void run() {
try{
final JSONObject json= new JSONObject(s);
handleSpecInfo(json);
}
catch (JSONException e) {
context.showMessage("Ошибка блин!");
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace(); 
}

}
}).start();

}
abstract public void handleSpecInfo(JSONObject json) throws UnsupportedEncodingException;
}


Забыл упомянуть UIContext — по сути это класс основного потока сообщений, то бишь EDT
Его функция здесь — вывод сообщения об ошибке (или иное действие в интерфейсе пользователя).
В качестве формата данных выбран json, но может быть и любой другой формат, все зависит от реализации
handleSpecInfo


Далее, дадим юзеру возможность убить подвисшее соединение, заодно добавим красоты, чтобы было видно, что выполняется запрос.

class Splash extends AbortConnectionAdapter{
Form splashform;
Splash(String splashText){
splashform= new Form();
splashform.refreshTheme();
TextArea ta= new TextArea(splashText);
ta.setAlignment(Form.CENTER);
ta.setEditable(false);
splashform.setLayout(new BorderLayout());
splashform.addComponent(BorderLayout.CENTER, ta);
splashform.addCommand(new Command("Отмена", 0));
splashform.addCommandListener(new ActionListener(){

public void actionPerformed(ActionEvent actionEvent) {
cancel();
// изменения в интерфейсе, например показ другой формы
}
});
}

public void showSplashForm() {
splashform.show();
}
}


Сплэш скрин примитивный — просто формочка с текстом и одной командой «отмена».
Далее можно наследовать этот класс, например если захочется переопределить метод
showSplashForm
, или делать что-то дополнительно в методе
cancel


Проверка того, что на наш запрос к серверу пришел ответ

public class WatchSheduler implements ConnectListener{
UIContext context;
Timer timer;
TimerTask task;
AbortConnection abortConnection;

final int watchdelay= 45000;
boolean isConnected= false;

public WatchSheduler(UIContext context, AbortConnection abortConnection) {
this.context = context;
this.abortConnection= abortConnection;
}

public void start(){
task= new TimerTask() {
public void run() {
if(!isConnected){
Display.getInstance().callSerially(new Runnable(){

public void run() {
if(abortConnection!= null)
abortConnection.cancel();
}
});
}
}
};
timer= new Timer();
timer.schedule(task, watchdelay);
}
public void connected() {
this.isConnected= true;
}
}


Теперь, собственно, класс соединения

public class Sender {
UIContext context;
String address;
ResponseHandler handler;
HttpRequest request;
static Vector activeThreads= new Vector();
ConnectListener connectListener;
AbortConnection abortConnection;

Sender(UIContext context, ResponseHandler handler, AbortConnection abortConnection){
this.context= context;
this.handler= handler;
this.abortConnection= abortConnection;
}

private void send(final HttpRequest request){
try {
abortConnection.setAbortedThread(Thread.currentThread()); 
connectListener= new WatchSheduler(context, abortConnection);
connectListener.start();
request.send();
byte[] response= request.getResponse();
connectListener.connected();
if(handler!= null && response!= null){
handler.handleResponse(response);
}

} catch (IOException e) {
e.printStackTrace();
context.showMessage("Ошибка раз: "+ e.getMessage());

} catch (SecurityException e) {
e.printStackTrace();
context.showMessage("Ошибка два: "+ e.getMessage());
context.exit();

}catch(OutOfMemoryError e){
e.printStackTrace();
context.showMessage("Ошибка три: "+ e.getMessage());
}catch(NullPointerException e){
e.printStackTrace();
context.showMessage("ошибка 4 "+ e.getMessage());
}
finally{
activeThreads.removeElement(Thread.currentThread()); 
}
}

void executeGet(String address, Hashtable map){
if(abortConnection!= null)
abortConnection.showSplashForm();
this.address= address;
request= new HttpGetRequest(map);
Thread thread= new Thread(new Runnable(){

public void run() {
send(request);
}
}, "get");
activeThreads.addElement(thread);
thread.start();
}

void executePost(String address, Hashtable map){
if(abortConnection!= null)
abortConnection.showSplashForm(); 
this.address= address; 
request= new HttpPostRequest(map);
Thread thread= new Thread(new Runnable(){

public void run() {
send(request);
}
}, "post");
activeThreads.addElement(thread);
thread.start();
}

abstract private class HttpRequest{
HttpConnection connection;
Hashtable map;

HttpRequest(Hashtable map) {
this.map= map;
}

abstract void send() throws IOException, NullPointerException;

byte[] getResponse() throws IOException {
InputStream is= null;
ByteArrayOutputStream baos= null;
byte[] response= null;
int rc= 0;
try{
rc = connection.getResponseCode();
if(rc!= 200)
throw new IOException("Какой-то код "+ rc);

if(handler!= null){
is= connection.openInputStream();
int c;
baos = new ByteArrayOutputStream();
while( (c= is.read())!=-1 )
baos.write( c );
response= baos.toByteArray();
}
}catch (IOException e){
try{
if(connection!= null) connection.close();
if(is!= null) is.close();
if(baos!= null) baos.close();
}catch (Exception ex){
ex.printStackTrace();
}
if(rc!= 200)
throw new IOException("Перенаправляем эксепшн наверх = "+ rc); 
}finally{
try{
if(connection!= null) connection.close();
if(is!= null) is.close();
if(baos!= null) baos.close();
}catch(Exception e){
e.printStackTrace();
}
}
return response; 
}
}

private class HttpPostRequest extends HttpRequest{

HttpPostRequest(Hashtable map) {
super(map);
}

void send() throws IOException, NullPointerException{
OutputStream os= null;
try{
connection = (HttpConnection) Connector.open(address, Connector.READ_WRITE);
connection.setRequestProperty("User-Agent", "Profile/MIDP-2.0 Configuration/CLDC-1.1");
connection.setRequestMethod(HttpConnection.POST);
JSONObject json= new JSONObject();
if(map!= null){
Enumeration names= map.keys();
while(names.hasMoreElements()){
String key= (String)names.nextElement();
String s= JavaEncoder.codeData((String)map.get(key));
json.put(key, s);
}
String jsonstring= json.toString();
connection.setRequestProperty("Content-Length", String.valueOf(jsonstring.length()));
os = connection.openOutputStream();
os.write(jsonstring.getBytes());
}

}catch (IOException e){
try{
if (os!= null) os.close();
}catch(IOException ee){
ee.printStackTrace();
}
throw e;
} catch (JSONException e) {
e.printStackTrace();
}
}
}

private class HttpGetRequest extends HttpRequest{

HttpGetRequest(Hashtable map) {
super(map);
}

void send() throws IOException, NullPointerException {
try{
String res= address+ "/?";
if(map!= null){
Enumeration e= map.keys();
while(e.hasMoreElements()){
String key= (String)e.nextElement();
String s= JavaEncoder.codeData((String)map.get(key));
res+=key+ "="+ s;
res+= "&";
}
res= res.substring(0, res.length()-1);
}
connection = (HttpConnection) Connector.open(res, Connector.READ_WRITE);
connection.setRequestMethod(HttpConnection.GET);
connection.setRequestProperty("User-Agent", "Profile/MIDP-2.0 Configuration/CLDC-1.1");
}catch (IOException e){
throw new IOException("HttpGetRequest");
}
}
}

static void dispose(){
for(int i=0; i< activeThreads.size(); i++){
Thread t= (Thread)activeThreads.elementAt(i);
if(t.isAlive()){
t.interrupt();
}
}
}
}


В класс
Sender
при создании передаются экземпляры парсера и убийцы потока. Далее в основном коде создается
Hashtable
и заполняется параметрами запроса, т.е. параметр-значение. Затем просто вызывается
executeGet()
или
executePost()
,
в который передается адрес и данные. Сразу же после отправки запроса запускается слежение за состоянием потока
connectListener= new WatchSheduler(context, abortConnection);
connectListener.start();


И, после получения ответа от сервера вызывается
connectListener.connected();

В случае, если ответа от сервера нет, а
WatchSheduler
запустил свой поток, то в нем произойдет вызов
abortConnection.cancel()
и поток соединения завершится. После чего, если мы используем не AbortConnectionAdapter а его наследника, могут быть выполнены дополнительные действия в пользовательском интерфейсе.

Итак, поток соединения может завершиться нормально и произойдут действия, прописанные в парсере ответа. Поток может быть отрублен автоматически по истечении времени ожидания или его может остановить пользователь.

Небольшой пример использования в коде, выполнение авторизации
void authorize(){

Sender sender= new Sender(this, new SimpleResponseHandler(this){
public void handleSpecInfo(JSONObject json) {
String id= json.optString("id");
if(id!= null){
Driver.ID= id;
driver.store();
createMainForm();

}
}
}, new Splash("Авторизация..."){

public void cancel() {
super.cancel();
createLoginForm();
}
});

Hashtable hashtable= new Hashtable();
hashtable.put("user", driver.login);
hashtable.put("pass", driver.pass);
sender.executePost(LOGIN_ADDRESS, hashtable);
}


0 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.