2014年6月11日

JAVA LDAP分頁查詢處理

這幾天遇到一個問題,是關於LDAP匯入的部分,客戶說有些人員沒有被匯入進來,查了一下log沒看到拋出任何Exception,透過LDAP Client軟體去下查詢,也查的到該位沒被匯入的人員,資料驗證的部分也都正確無誤,一時之間不知道問題在哪,找了幾個小時之後在log內發現到一個很奇怪的現象,為什麼匯入的人員數量會那麼剛好的為1000人,憑著這幾年寫程式的直覺來猜測,這裡面一定有什麼問題。

原本的寫法

原本的寫法應該是參考JAVA官網 Advanced Topics for LDAP - Creation 上的寫法,如下:

// set properties for our connection and provider
Properties properties = new Properties();
properties.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.ldap.LdapCtxFactory");
properties.put(Context.PROVIDER_URL, "ldap://192.168.150.129:389");
properties.put(Context.REFERRAL, "ignore");
properties.put(Context.SECURITY_PRINCIPAL,
        "cn=Manager,dc=maxkit,dc=com,dc=tw");
properties.put(Context.SECURITY_CREDENTIALS, "secret");

InitialDirContext context;
context = new InitialDirContext(properties);

透過JNDI去,來達成對LDAP Server的連線,上面的程式執行起來是正常的,也能夠正確的連上LDAP Server。

人員沒有匯入問題所在

那既然上面的code是正常的,那怎麼會出現人員沒匯入的狀況呢?

客戶用的是微軟的Active Directory,而在客戶透過AD Client軟體查詢給我們看時,我發現到他的軟體一次也是回一千筆資料,而我自己的程式在匯入時,也只處理剛好一千筆的資料,綜合上述幾點看來,有足夠的理由讓我懷疑Active Directory是不是會對查詢進行每次只回傳1千筆資料的分頁處理。

找了一些網站,沒找到官方的正式資料,倒是同事有找到類似的文章,只是文章內是教說如何加大Active Directory每次查詢回來的資料筆數(Increasing the number of objects returned in a single LDAP query),根據網路上查詢到的資料來推斷,就是這原因造成人員沒有成功匯入的!

解決方案

既然知道問題在於沒有做分頁,那就將之分頁,問題不就解決了?

因此上網查了一些JAVA處理LDAP分頁的範例,找到了幾篇好懂的解決方案,如 JNDI, Active Directory, Paging and Range Retrieval,不過也因此發現到,要用分頁的話,原本的寫法是不行的,原因在於原本寫法用的InitialDirContext這類別,沒有提供任何可以使用分頁的方法,因此根據找到的文章,改成使用InitialLdapContext來處理,就能進行分頁處理了,範例如下:

// LDAP連線相關設定
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_PRINCIPAL, "cn=Manager,dc=maxkit,dc=com,dc=tw");
env.put(Context.SECURITY_CREDENTIALS, "secret");
env.put(Context.REFERRAL, "ignore");
env.put(Context.PROVIDER_URL, LDAP_URL);

LdapContext ctx = new InitialLdapContext(env, null);

// 設定分頁相關資訊
int pageSize = 1000; //設定LDAP每次分頁所取的資料筆數
byte[] cookie = null;
ctx.setRequestControls(new Control[]{new PagedResultsControl(
    pageSize, Control.CRITICAL)});

do {
    // 設定 LDAP 人員查詢條件
    String searchFilter = "(objectClass=organizationalPerson)";
    SearchControls searchCtls = new SearchControls();
    searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);

    // 進行 LDAP 資料查詢與資料處理
    NamingEnumeration<SearchResult> results = ctx.search(
            "dc=maxkit,dc=com,dc=tw", searchFilter, searchCtls);
    while (results != null && results.hasMore()) {
        // ...
        // 這邊執行 LDAP 查出來的人員相關處理邏輯
        // ...
    }       

    //==================================================
    // 換頁處理開始
    //==================================================

    // 此分頁資料處理完畢,底下先取出cookie,
    // 如果cookie不為null,則表示還有下一頁的資料
    Control[] controls = ctx.getResponseControls();
    if (controls != null) {
        for (int i = 0; i < controls.length; i++) {
            if (controls[i] instanceof PagedResultsResponseControl) {
                PagedResultsResponseControl prrc = 
                    (PagedResultsResponseControl) controls[i];
                cookie = prrc.getCookie();
            }
        }
    }

    // 將cookie資訊提供給InitialLdapContext,讓它在接下來的查詢中進行換頁
    ctx.setRequestControls(new Control[]{new PagedResultsControl(
            pageSize, cookie, Control.CRITICAL)});

    //==================================================
    // 換頁處理結束
    //==================================================
} while (cookie != null);

ctx.close();

上面的程式需要注意到的是:

  1. 必須用InitialLdapContext才能進行操作分頁。
  2. 每次分頁取完時,要去取出cookie,並將之設定給InitialLdapContext,以讓他換頁查詢。

根據上面的程式,就能達成LDAP換頁查詢的需求。你可以透過一些變數記錄來觀察此段程式是否有確實的做換頁的動作,這邊為了精簡程式,因此就省略了。