2014年6月19日 星期四

Android 使用Volley發送RESTful request

Android Volley建置一文中筆者建立了Volley的jar檔但卻沒有提說怎麼使用,接下來說明如何使用Volley發送RESTful request吧

流程說明

1. 建立Volley的RequestQueue: 使用者將request放到RequestQueue後Volley會自動將request以非同步方式發送出去

2. 建立JsonObjectRequest, JsonArrayRequest或StringRequest: 依照target的RESTful service規格決定要用JSONObject, JSONArray或String其中之一吧~
註: 筆者採用 使用CodeIgniter架設RESTful Web Service 一文中架設的RESTful web service當做練習對象,其index.php同時提供GET, POST, PUT和DELETE的範例

3. 將第2步中產生的request放到RequestQueue中

4. 等待server的回應並進行處理


實作

1. 建立Android application project



2. 加入volley.jar到專案中

  建置volley.jar可參考Android Volley建置

3. 建立RequestQueue

  此處採用Singleton pattern建立Singleton class,其名稱為ApplicationController,詳細程式碼可參考"參考資料1",以下的程式碼筆者針對Singlegon做簡單的說明
public class ApplicationController extends Application {

    /**
     * Log or request TAG
     */
    public static final String TAG = "VolleyPatterns";

    /**
     * Global request queue for Volley
     */
    private RequestQueue mRequestQueue;

    /**
     * A singleton instance of the application class for easy access in other places
     * Singleton物件,簡單的說就是整個app只會有這一個ApplicationController物件,理想上不會產生第二份ApplicationController物件
     */
    private static ApplicationController sInstance;

    @Override
    public void onCreate() {
        super.onCreate();

        // initialize the singleton
        sInstance = this;
    }

    /**
     * @return ApplicationController singleton instance
     * 要使用ApplicationController一定要先透過此method取得ApplicationController物件
     */
    public static synchronized ApplicationController getInstance() {
        return sInstance;
    }

    /**
     * @return The Volley Request queue, the queue will be created if it is null
     */
    public RequestQueue getRequestQueue() {
        // lazy initialize the request queue, the queue instance will be
        // created when it is accessed for the first time
        if (mRequestQueue == null) {
            mRequestQueue = Volley.newRequestQueue(getApplicationContext());
        }

        return mRequestQueue;
    }

    /**
     * Adds the specified request to the global queue, if tag is specified
     * then it is used else Default TAG is used.
     * 
     * @param req
     * @param tag
     */
    public  void addToRequestQueue(Request req, String tag) {
        // set the default tag if tag is empty
        req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);

        VolleyLog.d("Adding request to queue: %s", req.getUrl());

        getRequestQueue().add(req);
    }

    /**
     * Adds the specified request to the global queue using the Default TAG.
     * 
     * @param req
     * @param tag
     */
    public  void addToRequestQueue(Request req) {
        // set the default tag if tag is empty
        req.setTag(TAG);

        getRequestQueue().add(req);
    }

    /**
     * Cancels all pending requests by the specified TAG, it is important
     * to specify a TAG so that the pending/ongoing requests can be cancelled.
     * 
     * @param tag
     */
    public void cancelPendingRequests(Object tag) {
        if (mRequestQueue != null) {
            mRequestQueue.cancelAll(tag);
        }
    }
}

4. 修改AndroidManifest.xml

  加入網路存取權限以及開啟app自動啟動ApplicationController
    <!-- 加入網路存取權限 -->
    <uses-permission android:name="android.permission.INTERNET" />
       
    <!-- 加入android:name這項屬性讓ApplicationController能在開啟app後自動建立ApplicationController-->
    <application
        android:name=".ApplicationController"
        android:allowBackup="true"
        .......
    />

5. 建立request

i. GET
private void doGet(){
    // Service的URL
    String url = "http://192.168.11.13/~shiun/ws/service/index";
    // 建立期望收到JsonObject的request
    // 若new JsonObjectRequest的第二個參數為null則代表是GET,反之若為JSONObject則為POST
    JsonObjectRequest request = new JsonObjectRequest(url, null, 
        new Listener<JSONObject>() {
            // 若server回傳HTTP status 200或204則會進入onResponse method 
            // 註:根據官網Issue 57527的討論,最新版應該是2xx都算request success
            @Override
            public void onResponse(JSONObject response) {
                Log.i(tag, response.toString());
            }
        }, new ErrorListener() { // Request失敗處理
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(tag, error.getMessage());
            }
    });
    // 將request放到RequestQueue,Volley會以非同步方式送出request
    ApplicationController.getInstance().addToRequestQueue(request);
}

GET成功~

ii. POST: POST的關鍵在於new JsonObjectRequest時要將POST的資料放到第二個參數中
private void doPost() throws JSONException {
    // Service的URL
    String url = "http://192.168.11.13/~shiun/ws/service/index";
    JSONObject param = new JSONObject();
    param.put("key1", "POST_value1");
    param.put("key2", "POST_value2");
    // 建立期望收到JsonObject的request
    // 因為第二個參數不是null,所以Volley把request當做POST傳送
    JsonObjectRequest request = new JsonObjectRequest(url, param, 
        new Listener<JSONObject>()
        // Request成功
            @Override
            public void onResponse(JSONObject response) {
                Log.i(tag, response.toString());
            }
        }, new ErrorListener() { // Request失敗處理
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(tag, error.getMessage());
            }
    });
    // 將request放到RequestQueue,Volley會以非同步方式送出request
    ApplicationController.getInstance().addToRequestQueue(request);
}

POST成功~

iii. PUT: PUT的關鍵點在於要new有method版本的JsonObjectRequest
private void doPut() throws JSONException
{
    String url = "http://192.168.11.13/~shiun/ws/service/index";
    JSONObject param = new JSONObject();
    param.put("key1", "PUT_value1");
    param.put("key2", "PUT_value2");
    // 這次的關鍵點在於使用Request.Method.PUT
    JsonObjectRequest request = new JsonObjectRequest(Request.Method.PUT, url, param,
        new Listener() {
            @Override
            public void onResponse(JSONObject response) {
                Log.i(tag, response.toString());
            }
        }, new ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(tag, error.getMessage());
            }
        });
    ApplicationController.getInstance().addToRequestQueue(request);
}

PUT成功~

iv. DELETE: DELETE時只需要把Method換成Request.Method.DELETE並配合service提供的URL即可
private void doDelete() throws JSONException
{
    String url = "http://192.168.11.13/~shiun/ws/service/index/1";
    JsonObjectRequest request = new JsonObjectRequest(Request.Method.DELETE, url, null,
        new Listener() {
            @Override
            public void onResponse(JSONObject response) {
                Log.i(tag, response.toString());
            }
        }, new ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(tag, error.getMessage());
            }
        });
    ApplicationController.getInstance().addToRequestQueue(request);
}

DELETE成功~


參考資料

1. Asynchronous HTTP Requests in Android Using Volley

2. Android Volley Tutorial - Making HTTP GET, POST, PUT, DELETE Requests

3. Application-element: "Android:name"

2014年6月9日 星期一

CodeIgniter 去除網址中的index.php

自從開始由CI寫PHP後每次都覺得那多餘的index.php很難看,尤其是之前在寫使用CodeIgniter架設RESTful Web Service這篇文章的時候GET, POST, PUT, DELETE的網址竟然是http://127.0.0.1/~shiun/ws/index.php/service/index,中間那index.php真的讓人受不了.........

這次就來把網址美化一下吧

1. 開啟mod_rewrite

Shiun@ShiunMac ~ sudo vi /etc/apache2/httpd.conf
啟動Apache的mod_rewrite模組 LoadModule rewrite_module libexec/apache2/mod_rewrite.so

2. 在CI的根目錄下新增.htaccess並加入以下內容

#IfModule代表module有啟動才會執行IfModule內的內容 <IfModule mod_rewrite.c> #啟動rewrite引擎 RewriteEngine On #這裡指的是CI的位置,也就是CI位於根目錄下的哪個路徑中 RewriteBase /ci/ #改寫規則一: 所有要往/ci/system下的request都導到index.php下 RewriteCond %{REQUEST_URI} ^system.* RewriteRule ^(.*)$ ./index.php?/$1 [L,QSA] #改寫規則二: 所有要往/ci/application下的request都導到index.php下 RewriteCond %{REQUEST_URI} ^application.* RewriteRule ^(.*)$ ./index.php?/$1 [L,QSA] #改寫規則三: 若網址後的參數不為index.php, images, css, js, robots.txt, favicon.ico任何一項 RewriteCond $1 !^(index\.php|images|css|js|robots\.txt|favicon\.ico) #且request的不是一般檔案 RewriteCond %{REQUEST_FILENAME} !-f #且request也不是資料夾 RewriteCond %{REQUEST_FILENAME} !-d #將request參數導到index.php下 RewriteRule ^(.*)$ ./index.php?/$1 [L,QSA] </IfModule> #如果mod_rewrite沒有啟動的話 <IfModule !mod_rewrite.c> # If we don't have mod_rewrite installed, all 404's # can be sent to index.php, and everything works as normal. ErrorDocument 404 /index.php </IfModule>

3. 重新啟動apache

Shiun@ShiunMac ~ sudo apachectl restart

4. 用新網址瀏覽網頁吧~,少了index.php感覺好看超多~


補充: 由於筆者的根目錄是/~shiun/ws/,因此RewriteBase這行要改成/~shiun/ws/才會動


mod_rewrite補充說明

  由於大多Google到的結果都沒針對mod_rewrite為什麼要這麼寫解釋,害筆者心中總少了啥,因此筆者另外收集並補充以上用到的mod_rewrite規則說明

  1. RewriteCond代表要滿足的條件,若滿足RewriteCond的條件則往下一行繼續執行

  2. 若連續出現數行RewriteCond且RewriteCond最後沒有提示OR, AND之類的文字,則預設為AND比對,例如"改寫規則三"就是連續三個RewriteCond的AND比對

  3. "改寫規則一"中出現的$1為 ^(.*)$ 這個正規表示式的結果,上述三個改寫規則的^(.*)$指的就是domain name之後的所有文字
    例如 aaa.org/index.php?a=b 中的index.php?a=b就是$1

  4. 每個改寫規則最後出現的[L, QSA]中的L代表這條改寫規則結束

  5. [L,QSA]的QSA代表將Query的參數補到網址的最後面
    例如 aaa.org/index.php?a=b&c=d,要是沒有QSA的話rewrite模組會把"?"後所有Query參數砍掉


參考資料
1. [CodeIgniter]如何去掉 URL 中的 index.php
2. 如何透過.htaccess檔,來使用URL redirect/rewrite功能?
3. .htaccess技巧: URL重写(Rewrite)与重定向(Redirect)

2014年6月8日 星期日

CentOS 6.5 虛擬機器(VM)換網卡後編號錯誤問題(eth0, eth1)

筆者今天在複製VirtualBox中的CentOS機器並給予新網卡號碼後,新的CentOS機器竟然沒自動更新eth0的網卡代號,取而代之的是新增了一個eth1

雖然應該無所謂但是編號越變越多也不是辦法,只好手動進行CentOS的網卡設定,以下筆記如何修改

1. 從VirtualBox的設定值中找到新的網卡MAC位置



2. 修改/etc/udev/rules.d/70-persistent-net.rules下的網卡設定

CentOS偵測到新網卡後自行產生eth1,但此時eth0的網卡已經不存在在機器上了

刪除掉舊的eth0整行設定後,把新的eth1設定中的NAME改為eth0,記得確定address是否為第1步中出現的MAC位置


3. 修改/etc/sysconfig/network-scripts/ifcfg-eth0中的MAC位置

將舊的eth0 MAC位置換為新的MAC位置

4. 重新啟動網路設定,打完收工~

[user@CentOSServer ~]$ sudo service network restart


2014年6月4日 星期三

Netbeans 手動加入javadoc的jar擋

由於筆者最近需要用Netbeans寫Java (不能用Eclipse不習慣啊QAQ),又寫Java的時候沒辦法在IDE上即時看doc真的很煩,只好抽時間來研究一下怎麼放含有javadoc的jar檔到Netbeans中

Netbeans即時顯示println的說明

以下以SLF4J為範例說明如何加入javadoc(若讀者沒聽過SLF4J可直接把SLF4J簡單想成println的另一個函式庫)

1. 加入slf4j-api-1.7.7.jar和slf4j-simple-1.7.7.jar到專案中



2. 加入以下用SLF4j寫的簡單程式碼

package test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Test {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(Test.class);
        logger.info("Hello World");
    }
    
}
在寫以上的code過程中就會看到Javadoc not found

3. 加入slf4j-api-1.7.7-javadoc.jar和slf4j-simple-1.7.7-javadoc.jar

  i. 對slf4j-api-1.7.7.jar按右鍵並選擇edit


  ii. 加入slf4j-api-1.7.7-javadoc.jar到Javadoc的欄位中
  
  iii. 同i和ii的步驟將slf4j-simple-1.7.7-javadoc.jar設定為slf4j-simple的javadoc

4. 重新寫一次logger.info會發現Netbeans終於跳出javadoc了! (WIndows的Netbeans使用者可透過ctrl + shift + space的快捷鍵叫出javadoc,但OS X上我找不到快捷鍵Orz...)



小知識

  在網路上下載的jar檔常常會看到xxx.jar, xxx-sources.jar, xxx-javadoc.jar,這些jar檔代表不同的意思

種類說明
xxx.jar已將.class檔案們打包好的檔案,xxx.jar也有可能已經包含source和javadoc(例如Android Volley建置出來的jar就包含了javadoc)
xxx-sources.jar包含source的jar檔,在debug時可以看到引用的jar檔的source code
xxx-javadoc.jar包含javadoc的jar檔,使用Netbeans開發時可以即時看到jar內某個class, method, ... etc. 的說明