Android : Connexion à OpenERP en json-rpc

...au travers du http et https
par Pierre Verkest, mis à jour le 21/10/2013

OpenERP expose ses services web en json-rpc, qui sont utilisés par le client web pour se connecter, récupérer le modèle des données, les données... Il est donc possible d’utiliser ce même protocole depuis Android pour se connecter et utiliser ces services.

Paramétrage du projet Android :

L'exemple ci dessous à été effectué avec l'IDE Eclipse en utilisant la plateforme 4.3 d'Android API niveau 18.

Il est possible de télécharger le projet d'exemple depuis le gestionnaire de source.

git clone https://bitbucket.org/petrus-v/openerpconnection.git

Il existe 2 branches :

  • Une branche pour la connexion http :
    git fetch && git checkout http-example
  • une deuxième pour tester la conexion https :
    git fetch && git checkout https-example

Ajouter la librairie json-rpc au projet en copiant le fichier android-json-rpc-0.3.4.jar dans le répertoire /libs/ de votre projet.

Dans le fichier manifeste /AndroidManifest.xml, ajouter l'élément suivant pour autoriser l'application à accéder à internet :

<uses-permission android:name="android.permission.INTERNET"/>

Connexion http :

Création de la classe asynchrone qui effectue la connexion à OpenERP, elle étend la classe AsynTask disponible dans le Framework d’Android.

/src/fr.anybox.openerpconnection/OpenERPConnection.java :

package fr.anybox.openerpconnection;

import org.alexd.jsonrpc.JSONRPCException;
import org.alexd.jsonrpc.JSONRPCHttpClient;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;

public class OpenERPConnection extends AsyncTask{
    static final String TAG = "OERP";
    static final String HOST = "http://test.openerp.verkest.fr"; //won't work, this url is not set as http
    static final String REQUEST_AUTHENTICATE = "/web/session/authenticate";
    static final String REQUEST_SEARCH_READ = "/web/dataset/search_read";
    static final String DATABASE = "oerp";
    static final String USER = "demo";
    static final String PASSWORD = "demo";
    static final String method = "call";
    
    @Override
    protected Void doInBackground(Context... params) {
        //The http client that allow to keep the session open over requests
        HttpClient httpClient = new DefaultHttpClient();
        
        JSONRPCHttpClient jsonRPC_client = new JSONRPCHttpClient(httpClient, HOST + REQUEST_AUTHENTICATE);
        jsonRPC_client.setConnectionTimeout(2000);
        jsonRPC_client.setSoTimeout(2000);
        jsonRPC_client.setEncoding("UTF-8");
        
        try {
            //set params to send to OpenERP to create the connection
            JSONObject jsonParams = new JSONObject();
            jsonParams.put("db", DATABASE);
            jsonParams.put("login", USER);
            jsonParams.put("password",USER);
            
            //Do the connection with openERP
            JSONObject json_result = jsonRPC_client.callJSONObject(method, jsonParams);
            Log.d(TAG, "We are connect to OpenERP, session id: " + json_result.getString("session_id"));
    
            //Now we can request partner using the same session (http client)
            jsonRPC_client = new JSONRPCHttpClient(httpClient, HOST + REQUEST_SEARCH_READ);
            jsonRPC_client.setConnectionTimeout(2000);
            jsonRPC_client.setSoTimeout(2000);
            jsonRPC_client.setEncoding("UTF-8");

            //get the user_context from the connection result, to send to OpenERP in the next request
            JSONObject context = json_result.getJSONObject("user_context");
            
            //set params to send to OpenERP service
            jsonParams = new JSONObject();
            jsonParams.put("session_id", json_result.getString("session_id"));
            jsonParams.put("context", context);
            jsonParams.put("model", "res.partner");
            jsonParams.put("limit",10);
            
            //Domain is use in openerp to define the where sql
            JSONArray domain =  new JSONArray();
            JSONArray customer = new JSONArray();
            JSONArray person = new JSONArray();
            //Get only customers
            customer.put("customer");
            customer.put("=");
            customer.put("1");
            domain.put(customer); 
            //Get only person (no companies)
            person.put("is_company");
            person.put("=");
            person.put("0");
            domain.put(customer); 
            //domain : [[customer, =, 1],[is_company, =, 0]]
            //SQL likes : WHERE customer = 1 AND is_company = 0
            jsonParams.put("domain",domain); 
            
            //choose fields you want to get
            JSONArray fields = new JSONArray();
            fields.put("name");
            fields.put("city");
            jsonParams.put("fields", fields);
            
            //jsonParams.put("offset",0);
            jsonParams.put("sort","write_date desc");
            
            //send the request to openerp
            json_result = jsonRPC_client.callJSONObject(method, jsonParams);
            JSONArray customers = json_result.getJSONArray("records");
            JSONObject oerp_customer;
            for(int i =0 ; i<10;i++){
                oerp_customer = customers.getJSONObject(i);
                Log.d(TAG, oerp_customer.getString("name") + " lives in " + oerp_customer.getString("city"));
            }    
            

        } catch (JSONException e) {
            Log.e(TAG,"Json exception: " + e.getMessage(), e);
        } catch (JSONRPCException e) {
            Log.e(TAG,"Json-rpc exception: " + e.getMessage(), e);
        }
        return null;
    }

}

Un bouton a été ajouté dans la définition de la vue de l'activité principale :

/res/layout/activity_main.xml :

...
<Button
    android:id="@+id/do_connection"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/do_connection" />
...

Enfin, dans la méthode onCreate de l’activité, l’événement onClick du bouton est redéfini.

Fichier /src/fr.anybox.openerpconnection/MainActivity.java :

...
    OpenERPConnection oc = new OpenERPConnection();
     
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // get the button and set it's listener
        Button connect = (Button)findViewById(R.id.do_connection);
        OnClickListener l = new OnClickListener() {
            
            @Override
            public void onClick(View v) {
                if(oc.getStatus()==Status.FINISHED){
                    //if AsyncTask is done, we have to create a new instance
                    oc = new OpenERPConnection();
                }
                if(oc.getStatus()== Status.PENDING){
                    // AsyncTask is ready to run
                    oc.execute(getApplicationContext());
                }else if(oc.getStatus()== Status.PENDING) {
                    //if AsynTask is already running, just wait and click again
                    Toast.makeText(getApplicationContext(), R.string.it_s_already_running_try_again_later,
                                    Toast.LENGTH_LONG ).show();
                }
            }
        };
        connect.setOnClickListener(l );
    }
...

La sortie LogCat :

10-21 23:08:10.568: D/OERP(3393): We are connect to OpenERP, session id: 548d76c73ecd47bca7e3dd37dd1d3505
10-21 23:08:19.528: D/OERP(3393): Pierre Verkest lives in Orléans
10-21 23:08:19.528: D/OERP(3393): Demo Portal User lives in false
10-21 23:08:19.528: D/OERP(3393): Thomas Passot lives in Wavre
10-21 23:08:19.528: D/OERP(3393): Axelor lives in Champs sur Marne
10-21 23:08:19.528: D/OERP(3393): Chamber Works lives in Detroit
10-21 23:08:19.528: D/OERP(3393): Millennium Industries lives in London
10-21 23:08:19.528: D/OERP(3393): Zhi Ch'ang lives in Shanghai
10-21 23:08:19.528: D/OERP(3393): Nebula Business lives in Rosario
10-21 23:08:19.528: D/OERP(3393): Paul Williams lives in Fremont
10-21 23:08:19.528: D/OERP(3393): Vauxoo lives in Caracas

Connexion https :

Pour une connexion https, avec un certificat auto-signé. Une méthode est de créer un fichier Bouncy Castle KeyStore (.BKS) contenant la liste des certificats de confiance des serveurs openERP auxquels vous devez vous connecter. Un outil comme portecle peut être utilisé pour générer ce fichier.

Dans les fichiers sources, un exemple de BKS est présent (/res/raw/verkest.bks), il contient le certificat auto-signé pour le site test.openerp.verkest.fr, le mot de passe du porteclé est verkest.

Le compte openerp utilisé pour cette connexion est demo / demo.

La classe AnyboxHttpsClient permet d'étendre les fonctionnalités de la classe DefaultHttpClient afin de se connecter à des serveur http ou https. Il ajoute le certificat auto-signé à la liste des certificats de confiance.

/src/fr.anybox.openerpconnection/AnyboxHttpsClient.java :

package fr.anybox.openerpconnection;

import java.io.InputStream;
import java.security.KeyStore;

import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.SingleClientConnManager;


import android.content.Context;


public class AnyboxHttpsClient extends DefaultHttpClient {
    final Context context;
    
    public AnyboxHttpsClient(Context context) {
        this.context = context;
    }
    

    @Override
    protected ClientConnectionManager createClientConnectionManager() {
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        // Register for port 443 our SSLSocketFactory with our keystore
        // to the ConnectionManager
        registry.register(new Scheme("https", newSslSocketFactory(), 443));
        return new SingleClientConnManager(getParams(), registry);
    }
 
    private SSLSocketFactory newSslSocketFactory() {
        try {
            // Get an instance of the Bouncy Castle KeyStore format
            KeyStore trusted = KeyStore.getInstance("BKS");
            // Get the raw resource, which contains the keystore with
            // your trusted certificates (root and any intermediate certs)
            InputStream in = context.getResources().openRawResource(R.raw.verkest);
            try {
                // Initialize the keystore with the provided trusted certificates
                // Also provide the password of the keystore
                trusted.load(in, "verkest".toCharArray());
            } finally {
                in.close();
            }
            // Pass the keystore to the SSLSocketFactory. The factory is responsible
            // for the verification of the server certificate.
            SSLSocketFactory sf = new SSLSocketFactory(trusted);
            // Hostname verification from certificate
            // http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e506
            // http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d5e445
            sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
            return sf;
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

}

Dans la méthode DoInBackground de la classe OpenERPConnection, remplacer la ligne de création du client http :

//The http client that allow to keep the session open over requests
HttpClient httpClient = new DefaultHttpClient();
Par
//The https client that allow to keep the session open over requests
HttpClient httpClient = new AnyboxHttpsClient();

Tester de nouveau, le résultat est le même qu'en http, si vous utilisez la même base.