יום שלישי, ינואר 11

חיבור לאינטרנט חלק 2: REST ו-JSON ו-Google Translate

זהו החלק בשני בסדרה שעוסקת בשרותי אינטרנט. מי שהנושא חדש עבורו, ואינו מכיר את HttpClient, מוזמן לקרוא את החלק הראשון.

היום נבחן את שרות התרגום של גוגל- Google Translate. נטפל ב-REST וב-JSON.
כמה מילים על   REST- Representational State Transfer
REST היא  ארכיטקטורה לתקשורת בין מערכות מבוזרות בה כל אחד המהמשאבים (resources) מיוצג בתוך ה-  URL. ה-REST יושב על גבי ה-HTTP ומשתמש בפונקציות שלו: POST,PUT,DELETE, GET וכו. ה-Payload של REST מיוצג בפורמט XML או JSON.
REST אינו סטנדרט: מימושים שונים שלו אינם זהים,  ומשופ כך מכנים אותם RESTful, בהיותם וריאציה עם עקרונות משותפים. REST הוא פרוטוקול קל משקל, עם מעט overhead, וקל לטיפול, בהשוואה לשיטה הנפוצה השניה - SOAP.
גם אם ה-SOAP נחשב הסטנדרט דה-פקטו להעברת הודעות בשרותי אינטרנט, כשמדובר על מכשירים ניידים, ה-REST אטרקטיבי יותר וצובר פופולריות בהתאם.
שרותי אינטרנט מבוססי REST נפוצים מאד, וכוללים בין השאר את AMAZON, GOOGLE, eBAY, Flickr ועוד.
הפרוטוקול של גוגל למשל, ה-GData, משתמש ב-Atom Publishing Protocol שהוא  RESTful. 
לגבי התמיכה של אנדרואיד ב-REST: אין באנדרואיד API יחודי ל-REST, אבל היות ש-REST  נשען על המתודות של HTTP, ה-HTTP API עושה את העבודה. מעבר לכך, היות שלא מדובר בסטנדרט נוקשה, העבודה מול כל שרת REST צריכה להיות מותאמת למימוש היחודי.

 כמה מילים על JSON -JavaScript Object Notation
JSON הוא פורמט של מבני נתונים. הוא קל משקל (בהשוואה לפורמט הנפוץ השני, ה-XML למשל), והוא כמובן לא תלוי שפת תכנות.

סוגי הנתונים שהוא יכול להכיל:

-מספרים
-String
-Boolean
-מערך : ערכים מופרדים עם פסיק, בתוך סוגריים מרובעם.
-אובייקט: אוסף של זוגות key:value בתוך סוגריים מסולסלים, מופרדים בינהם בפסיק.
-null.



הנה דוגמא לקובץ נתונים בפורמט JSON:



{"widget": {


"debug": "on",


"window": {


"title": "Sample Konfabulator Widget",


"name": "main_window",


"width": 500,


"height": 500


},


"image": { 


"src": "Images/Sun.png",


"name": "sun1",


"hOffset": 250,


"vOffset": 250,


"alignment": "center"


},


"text": {


"data": "Click Here",


"size": 36,


"style": "bold",


"name": "text1",


"hOffset": 250,


"vOffset": 100,


"alignment": "center",


"onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"


}


}} 


    האובייקט הנ"ל מתאר ווידג'ט.
    בתוך הווידג'ט שלושה אובייקטים:
    1. חלון התצוגה
    2. התמונה
    3. הטקסט.

    הנה אותו אובייקט בפורמט xml:


    <widget>
        <debug>on</debug>
        <window title="Sample Konfabulator Widget">
            <name>main_window</name>
            <width>500</width>
            <height>500</height>
        </window>
        <image src="Images/Sun.png" name="sun1">
            <hOffset>250</hOffset>
            <vOffset>250</vOffset>
            <alignment>center</alignment>
        </image>
        <text data="Click Here" size="36" style="bold">
            <name>text1</name>
            <hOffset>250</hOffset>
            <vOffset>100</vOffset>
            <alignment>center</alignment>
            <onMouseUp>
                sun1.opacity = (sun1.opacity / 100) * 90;
            </onMouseUp>
        </text>
    </widget>

    גוגל דוחפת את השימוש ב-REST ו-JSON ב-API שלה. הבשורה הטובה: אנדרואיד כולל parsers עבור XML וגם עבור JSON. נכיר אותם במהלך הדוגמאות שבסדרה זו.

    בחרתי את Google Translator להיות ה-API  הראשון אותו נבחן. הוא עושה שימוש ב-REST ו-JSON.
    Google Translator API
    ניגשתי לאתר של גוגל, ומצאתי שם את קטע קוד הג'אווה עבור בקשת ה-Translate. הנה הקוד משם:

    URL url = new URL("https://ajax.googleapis.com/ajax/services/language
    /translate?" +
                       
    "v=1.0&q=Hello,%20my%20friend!&langpair=en%7Ces&key=INSERT-YOUR-KEY&userip=
    INSERT-USER-IP"); 
     URLConnection connection = url.openConnection(); 
    connection.addRequestProperty("Referer", 
     /* Enter the URL of your site here */); 
    
    String line; 
    
    StringBuilder builder = new StringBuilder(); 
     BufferedReader reader = 
     new BufferedReader(new InputStreamReader(connection 
    .getInputStream()));
     
     
     
    while((line = reader.readLine()) != null) {
     builder.append(line);
    } 
    
    JSONObject json = new JSONObject(builder.toString()); 
     // now have some fun with the results...


    בטח ניחשתם על סמך ה-url,  שמדובר בבקשה לתרגום Hello מאנגלית לספרדית.  קטע הקוד הנ"ל לא משתמש  ב-Apache HostHttp, אבל  הפורמט של ה-request ברור.

    והנה הפורמט של התשובה - גם זה מתוך דף ההסבר של גוגל:

    {
      "responseData": {
        "translatedText": "Hola, mi amigo!"
      },
      "responseDetails": null,
      "responseStatus": 200
    }
    מכאן אפשר לבנות תוכנית משלנו לתרגום משפטים.

    תאור הדוגמא
    • נבצע גישה ל-Google Translator, לתרגום מאנגלית לעברית.
    • הטקסט באנגלית מוכנס לחלון EditText
    • התוצאה מוצגת ב-TextView
    • התחלת הפעולה ע"י לחיצה על כפתור.
    • הגישה לאינטרנט והטיפול בתשובה נעשים ב-Thread עצמאי, ששייך לActivity הראשי.


    צילום תמונת המסך






    לא בדיוק לתרגום הזה ציפיתי, אבל זו לא בעיה של REST או JSON.

    נעבור לתוכנית

    היא מורכבת משני Classes:
    1. Activity שמפעיל את הכל ומטפל ב-UI.
    2. Thread שמביא את האינפורמציה, מעבד אותה, ושולח אותה ל-UI ע"י שליחת runnable ל-Handler שפועל ב-UI Thread.
    בנוסף לקבצי הג'אווה:
      קובץ ה-Layout ללא שום חידושים. מוגדרים בו 2 ווידג'טים: כפתור, TextView ו- TextEdit.
      קובץ ה-Manifest: כולל שורת Internet License. לא לשכוח להוסיף אותה!
      שני הקבצים הנ"ל יחד עם כל שאר קבצי הפרויקט ניתנים להורדה - כרגיל -  בעזרת הלינק שבתחתית הדף.

      נתחיל עם WebTranslate - ה- Activity class.


      import android.app.Activity;
      import android.os.Bundle;
      import android.os.Handler;
      import android.view.View;
      import android.widget.Button;
      import android.widget.EditText;
      import android.widget.TextView;

      public class WebTranslate extends Activity {
          private EditText srcEditText;
          private TextView transTextView;
          private Handler mHandler = new Handler();
      1.     @Override
      2.     public void onCreate(Bundle savedInstanceState) {
      3.        super.onCreate(savedInstanceState);
      4.        setContentView(R.layout.main);
      5.        srcEditText = (EditText) findViewById(R.id.src_text);
      6.        transTextView = (TextView) findViewById(R.id.translated_text);
      7.        Button buttonTranslate = (Button) findViewById(R.id.translate_button);
      8.        buttonTranslate.setOnClickListener( new View.OnClickListener() {
      9.               public void onClick(View view) {
      10.                        String srcText = srcEditText.getText().toString().trim();
      11.                     transTextView.setText(R.string.wait_msg);
      12.                           WebTranslateThread webTranslateThread =
      13.                               new WebTranslateThread(WebTranslate.this, srcText);
      14.                           webTranslateThread.start();                }
      15.           });    
      16.     }
      17.     public void responseDisplay(final String text) {
      18.         mHandler.post(new Runnable() {
      19.             public void run() {
      20.                 transTextView.setText(text);
      21.              }
      22.           });
      23.     }
      24.  }





      קל לזהות את שתי המתודות ב-Activity.
      onCreate שכרגיל מופעלת עם הפעלת האפליקציה.
      שורה 4: יצירת ה-view עפי קובץ ה-layout ששמו main.xml.
      שורות 5-7: איתחול 3 הווידג'טים עפ"י ה-R.class. גם הם כמבון הוגדרו ב-main.xml.
      שורה 8-15: ה-onClickCallback של הכפתור:
      שורה 10: העברת הטקסט שהוקלד בחלון ל-String.
      שורה 11: הדפסת הודעת המתנה למסך. עם תום הפעולה תוחלף ההודעה הזו בתרגום שיתקבל.
      שורה 12: יצירת אובייקט של ה-Thread, וקריאה ל-constructor שלו. ה-constructor מקבל שני פרמטרים:
      האובייקט של ה-Activity,  כך שיוכל לקרוא לו בחזרה.
      ה-String לתרגום.
      שורה 14: הפעלת ה-Thread.
      עד כאן תאור ה-callbackובעצם סיימנו עם onCreate.

       responseDisplay, מופעלת מתוך ה-Thread. היא מעבירה את התוצאה שהתקבלה מה-Thread ל-UI. המעבר ל-UI Thread נעשה עם runnable.

      נעבור ל-class השני, שם עיקר ענייננו.
      WebTranslateThread

      import java.io.BufferedReader;
      import java.io.IOException;
      import java.io.InputStreamReader;
      import java.net.URI;
      import java.net.URISyntaxException;
      import java.net.URLEncoder;

      import org.apache.http.HttpResponse;
      import org.apache.http.client.HttpClient;
      import org.apache.http.client.methods.HttpGet;
      import org.apache.http.impl.client.DefaultHttpClient;
      import org.json.JSONException;
      import org.json.JSONObject;

      import android.util.Log;
      1. public class WebTranslateThread extends Thread {
      2.    private static final String TAG = "TranslateThread";
      3.    private final WebTranslate callerActivity;
      4.    private final String srcText;
      5.    WebTranslateThread(WebTranslate callerActivity, String srcText) {
      6.       this.callerActivity = callerActivity;
      7.       this.srcText = srcText;
      8.     }
      9.    public void run() {
      10.       String receivedText = responseProcess(httpTranslateGet(srcText));
      11.       callerActivity.responseDisplay(receivedText);
      12.       Log.d("TranslateThread ","id="+ Thread.currentThread().getId());
      13.    }
      14.    private String httpTranslateGet(String srcText) {
      15.        String line = "null";
      16.        StringBuilder builder = null;
      17.        try {
      18.            String q = URLEncoder.encode(srcText, "UTF-8");
      19.            String url = "http://ajax.googleapis.com/ajax/services/language/translate"
      20.             + "?v=1.0" + "&q=" + q + "&langpair=" + "en"
      21.             + "%7C" + "iw";
      22.            HttpResponse response=null;
      23.            HttpClient client = new DefaultHttpClient();
      24.            HttpGet request = new HttpGet();
      25.            request.setURI(new URI(url));
      26.            response = client.execute(request);
      27.            BufferedReader bufferReader =
      28.                 new BufferedReader (new InputStreamReader(response.getEntity().getContent()));
      29.            builder = new StringBuilder();
      30.            while((line = bufferReader.readLine()) != null) {
      31.                builder.append(line);
      32.            }
      33.            bufferReader.close();
      34.        }catch(URISyntaxException e){
      35.            Log.e(TAG," URISyntaxException");
      36.        }catch(IOException e){
      37.            Log.e(TAG,"IOException. jason or http");
      38.        }catch(IllegalStateException e){
      39.            Log.e(TAG,"Http IllegalStateException");
      40.        }
      41.        return builder.toString();
      42.        }
      43.        private String responseProcess(String line){
      44.            String response = "ארעה שגיאה";
      45.            if(null==line)
      46.                return response;
      47.            try{
      48.                JSONObject jsonObject = new JSONObject(line);
      49.                response = jsonObject.getJSONObject("responseData")
      50.                .getString("translatedText")
      51.                .replace("&#39;", "'")
      52.                .replace("&amp;", "&");
      53.            }catch(IllegalStateException e){
      54.                Log.e(TAG,"Http IllegalStateException");
      55.            }catch (JSONException e) {
      56.                Log.e(TAG, "JSONException");
      57.            }
      58.            return response;
      59.        }
      60. }
        ה-class מכיל 4 מתודות:
        1. Constructor.
        2. run - מופעל עם הפעלת ה-Thread, ודרכו מופעלות שתי המתודות הבאות, בהן מתבצעת העבודה:
        3. httpTranslateGet- מבצעת את ה-Http Get, ומביאה מהשרת את תוצאת התרגום.
        4. responseProces- שולפת את תוצאת התרגום מתוך ה-JSON ומעבירה ל-UI.


          נתמקד בשתי המתודות האחרונות. נתחיל בראשונה.
          httpTranslateGet
          כפי שניתן היה לצפות, היא דומה מאד למתודה שהכרנו כבר בפוסט הקודם. 

          • העברת הטקסט לפורמט UTF-8, שורה 18.
          • שימו לב ל-url. הוא נבנה על סמך הדוגמא ששלפתי מהאתר של גוגל - ראו למעלה. קוד שפת היעד הוחלף לעברית-iw.שורה 19.
          • ניצור -  HttpClient, שורה 22.
          • ניצור אובייקט פעולה ל-request מסוג Get - שורה 23.
          • נדביק ל-request את ה-url - שורה 24.
          • ונפעיל...שורה 25.
          • נעביר את התוצאה ל-buffer - שורה 27.
          •  נשלוף  את התוצאה מה-buffer תוך שימוש ב-StringBuilder. היא תועבר למתודה הבאה, שמבצעת parsing על קובץ ה-JSON שקיבלנו. שורות 29-31.
           responseProcess

          התהליך כאן קצר:
          יצירת אובייקט JSON.
          JSONObject jsonObject = new JSONObject(line);
          שליפת ה-String מתוך האובייקט:
                         response = jsonObject.getJSONObject("responseData")
                         .getString("translatedText")
          החלפת יצוגי ה-ASCII בתווים.

                         .replace("&#39;", "'")
                         .replace("&amp;", "&");


          אפשר כמובן להרחיב את האפליקציה עבור שפות נוספות.
          הערה: שדה ה-response status לא טופל דוגמא הנ"ל. במצב תקין צריך להכיל 200.
          קישור להורדת קבצי הפרויקט.



          9 תגובות:

          1. תודה הסבר מצויין, אבל אולי שווה להוסיף גם הסבר על SOAP xml, אם אתה רוצה אני אשלח לך דוגמאות מלאות (חקרתי לעומק).

            השבמחק
          2. הי לאוניד. תודה. שלח בבקשה, אשמח לשלב את זה + תקבל כמובן קרדיט.

            השבמחק
          3. אהלן רונן,
            אחלה פוסט, מאוד עוזר להבין את כל הענין.
            יש רק בעיה עם הקישור להורדת הקבצים, תוכל לתקן אותו בבקשה?
            תודה מראש

            השבמחק
          4. אהלן מאיר,
            תודה על המשוב.
            תיקנתי, מקווה שזה בסדר עכשיו.
            רונן.

            השבמחק
          5. נ.ב.
            מאיר, להזכירך, הערב ב18:30 מפגש מפתחים במכללה האקדמית ת"א. ראה קישור בצד ימין למעלה.


            לינק לאירוע בפייסבוק:
            https://www.facebook.com/event.php?eid=206762096007617

            השבמחק
          6. היי, הסבר יפה מאוד.
            השלב המעניין מתחיל כאשר מנסים להעביר אובייקטים מורכבים, ובמיוחד מערך פולימורפי (לדוגמא, מערך של צורות שכל אובייקט יכול להיות צורה מסוג שונה).

            חקרתי את זה במשך כמה ימים עד שמצאתי את הפתרון האופטימאלי... אם אי פעם תתקל בצורך כזה, אשמח לעזור (;

            אלירם.

            השבמחק
          7. אהלן רונן ,
            אחלה מדריך, יש אפשרות לעלות את המדריך של הsoap של לאוניד?
            עם העברת אובייקטים מהweb services אל הלקוח?

            השבמחק
          8. אהלן מרט,
            הנה המדריך של לאוניד. בגלל מגבלת אורך התגובות פיצלתי אותו בין כמה תגובות. במקרה שהוא ייושר לימין, ממליץ להעתיק אותו ל-editor.
            רונן.

            @Override
            public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.login);
            final soapHttpConnect soapConnect = new soapHttpConnect();
            final xmlParser XMLParser = new xmlParser();

            ...

            String result = soapConnect.call( id.getText().toString(), password.getText().toString(),null);
            ...

            String LOGIN_STATUS = XMLParser.parseLOGIN_STATUSData(result);

            ....
            }

            הקובץ השני זה הבנייה של בקשת ה SOAP ושליחתה לשרת וקבלת תשובה

            public class soapHttpConnect {


            public String call( String user , String password,String option) {

            String result = null;



            HttpParams httpParameters = new BasicHttpParams();
            // Set the timeout in milliseconds until a connection is established.
            int timeoutConnection = 15000;
            HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
            // Set the default socket timeout (SO_TIMEOUT)
            // in milliseconds which is the timeout for waiting for data.
            int timeoutSocket = 35000;
            HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);

            DefaultHttpClient httpclient = new DefaultHttpClient(httpParameters);

            HttpPost httppost = new HttpPost("לאיפה שאתה שולח את הבקשה");

            httppost.setHeader("SOAPAction", " ניתן לבדוק עלידי ?WSDL SOAPAction מה שאמור להיות מוגדר ב");
            httppost.setHeader("Content-Type", "text/xml; charset=utf-8");

            try {
            HttpEntity entity = new StringEntity(soapRequest(body(Integer.valueOf(reqID),user,password,option)),HTTP.UTF_8);
            httppost.setEntity(entity);
            HttpResponse response = httpclient.execute(httppost);
            HttpEntity r_entity = response.getEntity();



            if(r_entity.isStreaming()&&r_entity != null)
            {

            //soapXMLParser soapParser = new soapXMLParser();
            //result = soapParser.parse(new DataInputStream(r_entity.getContent()));
            xmlParser xp = new xmlParser();
            result = xp.parseOutputData(new DataInputStream(r_entity.getContent()));
            }
            httpclient.getConnectionManager().shutdown();

            } catch(Exception E) {
            return null;
            }




            return result;
            }



            private String soapRequest(String body)
            {
            final StringBuffer soap = new StringBuffer(); // Comment: 4
            soap.append("\n");
            soap.append("\n");
            soap.append( "\n");
            soap.append( "\n");
            soap.append( "");
            soap.append( body);
            soap.append( "");
            soap.append( "\n");
            soap.append( "\n");
            soap.append("");

            return soap.toString();

            }
            private String body( String user , String password,String option)
            {

            StringBuffer body = new StringBuffer();

            body.append("" + ... + "");

            ...

            return body.toString();
            }


            }

            השבמחק
          9. המשך המדריך של לאוניד:




            בסוף יש את SAXPARSER לניתוח התשובה , בקובץ שלישי :

            public class xmlParser {


            public String parseLOGIN_STATUSData(String content){

            LOGIN_STATUSHandler handler = new LOGIN_STATUSHandler();
            String result = new String();
            XMLReader xr;

            SAXParserFactory spf = SAXParserFactory.newInstance();


            try {
            SAXParser sp = spf.newSAXParser();

            xr = sp.getXMLReader();

            xr.setContentHandler(handler);
            InputSource is = new InputSource(new StringReader(content));
            xr.parse(is);

            result = handler.getMessages();
            } catch (SAXException e) {
            e.printStackTrace();
            } catch (IOException e) {
            e.printStackTrace();
            } catch (ParserConfigurationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            }
            return result;
            }

            }

            ואת קובץ ה HANDEL שמתפל בניתוח

            public class LOGIN_STATUSHandler extends DefaultHandler{

            private String result;
            @SuppressWarnings("unused")
            private String currentMessage;
            private StringBuilder builder;

            public String getMessages(){
            return this.result;
            }

            @Override
            public void characters(char[] ch, int start, int length)
            throws SAXException {
            super.characters(ch, start, length);
            builder.append(ch, start, length);
            }

            @Override
            public void endElement(String uri, String localName, String name)
            throws SAXException {
            super.endElement(uri, localName, name);

            if (localName.equalsIgnoreCase("RECORD")){
            currentMessage = builder.toString();
            } else if (localName.equalsIgnoreCase("LOGIN_STATUS")){
            result = builder.toString();
            }
            builder.setLength(0);

            }

            @Override
            public void startDocument() throws SAXException {
            super.startDocument();
            builder = new StringBuilder();
            }

            @Override
            public void startElement(String uri, String localName, String name,
            Attributes attributes) throws SAXException {
            super.startElement(uri, localName, name, attributes);
            if (localName.equalsIgnoreCase("MSG_TITLE")){
            this.currentMessage = new String();
            }
            }

            }

            השבמחק