Lotus Notes/Domino 環境下的 OpenSocial 開發

genusBIT發表於2010-09-25
劉 奇, 軟體工程師, IBM
李 三紅, 高階軟體工程師, IBM

簡介: 隨著社交網路的不斷髮展,使用者關係資訊已經成為一類重要的網路資料。OpenSocial 為構建跨多個網站的社交應用程式提供了一組通用 API,Apache Shindig 是 OpenSocial 規範的引用實現,旨在幫助 OpenSocial 開發人員快速構建自己的 OpenSocial 應用平臺。本文通過實際的例子,指導讀者如何通過 Shindig SPI 擴充套件,把 Lotus Notes/Domino NSF 資料庫中的使用者資料適配到 Shindig,建立基於 NSF 資料來源的 OpenSocial 容器。

預備知識

什麼是 OpenSocial

OpenSocial 是基於開放標準的一組通用的 API,用於幫助 WEB 的開發者構建跨多個社交網站的可移植的社交應用程式。OpenSocial 提供開發者一套通用的 API,基於該通用 API 開發的社交應用程式可以執行在任意支援 OpenSocial 規範的社交網站上。

關於更多的有關 OpenSocial 內容,請讀者參見 www.opensocial.org.

圖 1 是基於 OpenSocial 平臺的應用架構略圖:


圖 1. OpenSocial Architecture at a glance( 引自 Chris Schalk@GoogleTM)
圖 1. OpenSocial Architecture at a glance( 引自 Chris Schalk@GoogleTM)

一般來講,OpenSocial 的客戶端就是內嵌 OpenSocial 應用程式的 (OpenSocial Gadget) 的 WEB 頁面,OpenSocial Gadget 是對 iGoogle Gadget 的擴充套件,其包括了訪問 OpenSocial 資料 JavaScript. API。OpenSocial JavaScript. API 支援三種型別的核心 OpenSocial 服務, 即:People and Friends 資料 API, Activities 資料 API 和 Persistence 資料 API。 客戶端與 OpenSocial 伺服器使用基於 JSON 資料格式的 Request/Response 協議交換資料。

Apache Shindig

Shindig 是 OpenSocial 規範的引用實現,其主要的元件包括 :

關於更多的有關 Apache Shindig 內容,請讀者參見 http://incubator.apache.org/shindig.

Shindig 是如何工作的

OpenSocial 標準支援 REST 及其 RPC 兩種標準的 Web Services 協議。OpenSocial REST API 支援三種資源的表述方式,即:JSON,XML 和 Atom。OpenSocial JSON-RPC API 使用 HTTP   POST 方法與 OpenSocial 伺服器互動基於 JSON 格式的資料內容。當你在內嵌於 Web 頁面的 OpenSocial Gadget 中使用 JavaScript. API 獲得資料是,其底層使用了 JSON-RPC 協議。圖 2 顯示了該請求執行的流程。


圖 2. Shindig REST/RPC API 執行流程
圖 2. Shindig REST/RPC API 執行流程

  1. Shindig 的 Servlet 開始處理來自客戶端的請求。JsonRpcServlet 負責 RPC 請求,而 DataServiceServlet 負責 REST 請求。
  2. Servlet 通過 HandlerRegistry 獲得相應的 Handler。例如,如果你當前請求的是有關 Person 資料,Servlet 獲得 PersonHandler 的引用。
  3. 在獲得相應的 Handler 後,事實上 RequestItem 已經被隱式建立,並且傳遞給該 Handler。
  4. PersonHandler,ActivityHandler, 或者 AppDataHandler 接到請求進行處理,將返回給呼叫者 POJO(Plain Old Java Object) 資料集合。
    1. PersonHandler, 負責存取 Person 相關資料,比如使用者的 Profile 資訊。
    2. ActivityHandler, 負責存取 Activity 相關資料,比如 Activity 的建立,刪除等。
    3. AppDataHandler, 負責存取用於 Gadget 的個性化儲存資料。
  5. Handler 呼叫相應的 SPI(Shindig Service Provider) 獲得 Social 資料。
    1. PersonService, 提供呼叫給 PersonHandler。
    2. ActivityService, 提供呼叫給 ActivityHandler。
    3. AppDataService, 提供呼叫給 AppDataHandler。
  6. Servlet 在獲得 POJO 的資料集後,使用 Converter 轉換成使用者需要的格式,比如 JSON,然後輸出到 Response 流。
 

Guice 簡介

Guice 是一個輕量級,基於 Java5(主要運用泛型與註釋特性)的依賴注入 (dependency

injection) 框架。Guice 非常小而且快。Guice 是型別安全的,它能夠對建構函式,屬性,方法進行注入。

讀者可能奇怪,為什麼我們在這裡提及 Guice。Shindig 實現使用了 Guice 作為依賴注入框架,我們在下一節的實現中會用到 Guice。

那麼 Guice 是如何實現動態依賴注入的?如果使用 Guice,你的應用需要實現 Modules,Guice 傳遞 Binder 給你的 Module,你的 Module 使用 Binder 完成從介面 (interfaces) 到實現的對映,如清單 1 所示:


清單 1. Guice Binder

				
public class MyModule implements Module {
    public void configure(Binder binder) {
        binder.bind(Service.class).to(ServiceImpl.class).in(Scopes.SINGLETON);
    }
}

MyModule 的 configure 方法告訴 Guice 如何進行依賴注入,而通過 @Inject 註釋,我們可以告訴 Guice 在哪裡注入,你可以使用 @Inject 來標記任何你想要注入的建構函式,方法,物件域。如清單 2 所示:


清單 2. Guice @Inject

				
public class Client {
    private final Service service;

    @Inject
    public Client(Service service) {
        this.service = service;
    }

    public void go() {
        service.go();
    }
}

構建 N/D 環境下的 OpenSocial 應用平臺

實現架構


圖 3. 建立基於 NSF 資料來源的 OpenSocial 容器執行流程
圖 3. 建立基於 NSF 資料來源的 OpenSocial 容器執行流程

如圖 3 所示,建立基於 NSF 資料來源的 OpenSocial 容器是向客戶端提供了訪問 NSF 資料庫的 PersonService,這樣客戶端能夠訪問 NSF 資料庫中的 People 相關的資料。在本文的例項中,我們首先通過 Shindig SPI 建立基於 Domino 伺服器上目錄資料庫(即地址簿 names.nsf)的 OpenSocial 容器,使用者可以使用統一的 OpenSocial API(例子中我們建立了一個 Gadget)去訪問 Domino 地址簿上的使用者名稱字、郵件、電話和公司等資訊。

 

Domino 地址簿到 OpenSocial 資料模型的對映


圖 4. Domino 地址簿到 OpenSocial 資料模型對映圖
圖 4. Domino 地址簿到 OpenSocial 資料模型對映圖

前文已經提到,本文例項將通過 Shindig SPI 擴充套件基於 Domino 地址簿的 OpenSocial 容器,圖 4 則描述了 Domino 地址簿到 OpenSocial 資料模型的對映。值得注意的是,org.apache.shindig.social.opensocial.model.Person 是 Shindig 中一個很重要的介面,描述了所有 Person - 人公共的相關屬性,org.apache.shindig.social.opensocial.model.PersonImpl 則是它的一個實現類,例項中我們將地址簿中文件(即每條地址記錄)的 UNID(即唯一 ID)對映到 PersonImpl 中的 id。

Shindig 作為 OpenSocial 規範的引用實現,提供了 SPI 的擴充套件能力,允許你把資料適配到 Shindig 容器中去。前面提到 Shinding 提供了三種 DataRequestHandler s : PersonService,ActivityService,AppDataService。本例項實現 PersonService 介面向 Shindig 提供 Domino 資料庫中 People/Friends 相關的 OpenSocial 資料,如清單 3 所示,SocialNotesAddressBookJsonPersonService 實現了 PersonService 兩個介面方法 getPeople 及其 getPerson。getPeople 根據傳入引數 userIds,返回相應於該 id 列表的使用者列表,而 getPerson 根據傳入引數 id,返回相應於該 id 的使用者,在下一小節中將介紹這兩個方法何時被呼叫。

另外,Shindig 依賴 Guice 做動態的依賴注入 (dependency Injection),我們需要在 org.apache.shindig.social.sample.SampleModule 裡指示 Guice 把 PersonService 繫結到 SocialNotesAddressBookJsonPersonService,如清單 4 所示。


清單 3. PersonService 實現程式碼

				
public class SocialNotesAddressBookJsonPersonService implements PersonService {
    private String _dbLocation = null;
    private Database _db = null;
    private Session _session = null;
    private String _defaultView = "People";

    private final static String NAME_COLUMN_ALIAS = "FullName";
    private final static String MAIL_COLUMN_ALIAS = "InternetAddress";
    private final static String PHONE_COLUMN_ALIAS = "OfficePhoneNumber";
    private final static String COMPANY_COLUMN_ALIAS = "CompanyName";

    public Database getDb() {
        if(_db == null) {
            if(_dbLocation != null) {
                try {
                    if(_session == null)
                // 為了實現的簡單,我們在這裡將 Domino 伺服器的地址和訪問密碼寫在這裡
                        _session = NotesFactory.createSessionWithFullAccess("xxxxxxx");

                    _db = _session.getDatabase("server my ip address", _dbLocation);
                    if(!_db.isOpen())
                        _db.open();
                }
                catch (NotesException e) {
                    e.printStackTrace();
                }
            }

        }
        return _db;
    }

    @Inject
    public SocialNotesAddressBookJsonPersonService(
            @Named("shindig.socialtest.nsf.db") String location)
            throws Exception {
        _dbLocation = location;
    }
     public SocialNotesAddressBookJsonPersonService() {
    }

    @SuppressWarnings("unchecked")
    public Person convertTo(ViewEntry entry) {
        Person person = null;
        try {
            Document doc = entry.getDocument();
            if(doc != null)      {
                person = new PersonImpl();

                Vector namesVec = doc.getItemValue(NAME_COLUMN_ALIAS);
                if(namesVec.size() > 0 && !"".equals(namesVec.get(0))) {
                    Name name = new NameImpl();
                    String nameString = namesVec.get(0).toString();
                    lotus.domino.Name given = _session.createName(nameString);
                    if(given != null)
                        nameString = given.getAbbreviated();
                    name.setGivenName(nameString);
                    person.setName(name);
                }

                Vector idVec = doc.getItemValue(MAIL_COLUMN_ALIAS);
                if(idVec.size() > 0 && !"".equals(idVec.get(0))) {
                    List mails = new ArrayList();
                    ListField field = new ListFieldImpl(
                        "Notes ID",idVec.get(0).toString());
                    field.setPrimary(true);
                    mails.add(field);
                    person.setEmails(mails);
                    person.setId(idVec.get(0).toString());
                }

                Vector phoneVec = doc.getItemValue(PHONE_COLUMN_ALIAS);
                if(phoneVec.size() > 0 && !"".equals(phoneVec.get(0))) {
                    List phoneNumbers =
                        new ArrayList();
                    ListField field = new ListFieldImpl(
                        "Business Phone",phoneVec.get(0).toString());
                    phoneNumbers.add(field);
                    person.setPhoneNumbers(phoneNumbers);
                }

                Vector companyVec = doc.getItemValue(COMPANY_COLUMN_ALIAS);
                if(companyVec.size() > 0 && !"".equals(companyVec.get(0))) {
                    List rganizations =
                        new ArrayList();
                    Organization field = new OrganizationImpl();
                    field.setName(companyVec.get(0).toString());
                    organizations.add(field);
                    person.setOrganizations(organizations);
                }

                DateTime modified = doc.getLastModified();
                if(modified != null) {
                    Date updated = modified.toJavaDate();
                    person.setUpdated(updated);
                }
                person.setId(doc.getUniversalID());
            }
        }
        catch (NotesException e) {
            e.printStackTrace();
        }

        return person;
    }

    public Future>
            getPeople(Set userIds,
            GroupId groupId, CollectionOptions options, Set fields,
            SecurityToken token) throws ProtocolException {
        List result = Lists.newArrayList();

        NotesThread.sinitThread();
        Database notesdb = this.getDb();
        Person person = null;

        try {
            View view = notesdb.getView(_defaultView);
            ViewEntryCollection collection = view.getAllEntries();
            ViewEntry first = collection.getFirstEntry();
            while(first != null) {
                person = convertTo(first);
                if(person != null)
                    result.add(person);
                ViewEntry tmp = collection.getNextEntry();
                first.recycle();
                first = tmp;
            }
        }
        catch (NotesException e) {
            e.printStackTrace();
        }
        finally {
            try {
                if(notesdb != null)
                    notesdb.recycle();
            } catch (NotesException e) {
                e.printStackTrace();
            }
            NotesThread.stermThread();
        }
        int totalSize = result.size();

        return ImmediateFuture.newInstance(new RestfulCollection(
                result, options.getFirst(), totalSize, options.getMax()));
    }

    public Future getPerson(UserId id, Set fields,
            SecurityToken token) throws ProtocolException {
        NotesThread.sinitThread();
        Database notesdb = this.getDb();
        Person person = null;

        try {
            View view = notesdb.getView(_defaultView);
            ViewEntryCollection collection = view.getAllEntries();
            ViewEntry first = collection.getFirstEntry();
            Person tmpPerson = null;
            while(first != null) {
                tmpPerson = convertTo(first);
                if(id != null && first.getUniversalID()
                    .equals(id.getUserId(token))) {
                    person = tmpPerson;
                    break;
                }
                ViewEntry tmp = collection.getNextEntry();
                first.recycle();
                first = tmp;
            }
        }
        catch (NotesException e) {
            e.printStackTrace();
        }
        finally {
            try {
                if(notesdb != null)
                    notesdb.recycle();
            } catch (NotesException e) {
                e.printStackTrace();
            }
            NotesThread.stermThread();
        }
        return ImmediateFuture.newInstance(person);
    }

}


清單 4. SampleModule 程式碼
				
public class SampleModule extends SocialApiGuiceModule {

    protected void configure() {
    super.configure();

    bind(String.class).annotatedWith(Names.named("shindig.socialtest.nsf.db"))
				   .toInstance("names.nsf");
				// 繫結字串
				shindig.socialtest.nsf.db

    bind(ActivityService.class).to(JsonDbOpensocialService.class);
    bind(AppDataService.class).to(JsonDbOpensocialService.class);
    bind(PersonService.class).to(SocialNotesAddressBookJsonPersonService.class); // 繫結

    //PersonService
    bind(MessageService.class).to(JsonDbOpensocialService.class);
    bind(OAuthDataStore.class).to(SampleOAuthDataStore.class);

    // We do this so that jsecurity realms can
    // get access to the jsondbservice singleton
    requestStaticInjection(SampleRealm.class);
  }

  protected Set getHandlers() {
    ImmutableSet.Builder handlers = ImmutableSet.builder();
    handlers.addAll(super.getHandlers());
    handlers.add(SampleContainerHandler.class);
    return handlers.build();
  }

}

實現了一個新的 PersonService – 能訪問 Domino 地址簿中人的資訊,這樣使用者可以通過標準的 OpenSocial REST API 來訪問這些資訊,下面講介紹客戶端的實現。

清單 5 顯示了一個 Gadget 的實現,fetchPeople 使用了 osapi 獲得 OpenSocial 資料,並把它們展示在 HTML 頁面上。清單 6 則是顯示該 Gadget 的 HTML 頁面程式碼,執行結果如圖 5 所示。

讀者可以參考清單 5 中 fetchPeople 的註釋,標示 1 處將呼叫 PersonService 中的 getPerson 來獲取 Viewer 的資訊,而標示 2 處將呼叫 PersonService 中的 getPeople 獲取地址簿中所有使用者資訊,值得注意的是,這些操作真正執行是在標示 3 處。

接下來就是如何執行這個 Gadget 了。啟動 Shindig,瀏覽器中輸入 http://localhost:8080/gadgets/files/samplecontainer/socialtest.html 並執行,點選 List Contacts,效果如圖 5 所示。


清單 5. Gadget 實現

				

 
     

    

     

     
     
             .sr_outertable {
                   border : 2px solid #ffffff;
                 cellpadding:2px;
                 cellspacing:2px;
                 border-collapse:collapse;
              }
              .sr_outertable th{
                   background : #0B3861;
                 color:white;
                 fond-weight:bold;
                 text-align:center;
              }
              .sr_outertable_tr0 {
                   background : #EFF5FB;
                 fond-weight:bold;
                 text-align:center;
              }
              .sr_outertable_tr1 {
                   background : #FFFFFF;
                 fond-weight:bold;
                 text-align:center;
              }
         

         
function showResults(data){ showUISection('main'); var viewer = data.viewer; var titleElement = document.getElementById("feedtitle"); var nameNode = document.createTextNode(viewer.id); titleElement.appendChild(nameNode); allPeople = data.viewerFriends.list; for(var i = 0; i < allPeople.length; i++) { entry = allPeople[i]; var rowElement = document.createElement('tr'); rowElement.setAttribute('class', 'sr_outertable_tr'+ (i%2)); document.getElementById("tblResult").appendChild(rowElement); var nameTd = document.createElement('td'); var mailTd = document.createElement('td'); var phoneTd = document.createElement('td'); var companyTd = document.createElement('td'); //name var name = entry.name; if(name) { nameTd.appendChild(document.createTextNode(name.givenName)); rowElement.appendChild(nameTd); } //email var emails = entry.emails; if (emails){ for(var j = 0; j < emails.length; j++) { var mail = emails[0]; //work mail if(mail.type == 'Notes ID') { mailTd.appendChild(document.createTextNode(mail.value)); rowElement.appendChild(mailTd); } } } //phone var phones = entry.phoneNumbers; if(phones) { for(var j = 0; j < phones.length; j++) { var phone = phones[0]; if(phone.type == 'Business Phone') { phoneTd.appendChild(document.createTextNode(phone.value)); rowElement.appendChild(phoneTd); } } } //company var companies = entry.organizations; if(companies) { for(var j = 0; j < companies.length; j++) { var company = companies[0]; if(company.name) { companyTd.appendChild(document.createTextNode(company.name)); rowElement.appendChild(companyTd); } } } } } function fetchPeople() { var fields = ['id','age','name','gender','profileUrl','thumbnailUrl']; var batch = osapi.newBatch(); // 1、獲取或者 Viewer 相關資訊 batch.add('viewer', osapi.people.getViewer({sortBy:'name',fields:fields})); // 2、獲取地址簿中所有使用者資訊 batch.add('viewerFriends', osapi.people .getViewerFriends({sortBy:'name',fields:fields})); // 3、真正執行這些操作 batch.execute(showResults); } function showUISection(area){ var availableareas = ['main']; for (var i=0;i ]]>


清單 6. HTML 顯示程式碼
				

 
 A very simple OpenSocial Gadget testing container
 
 
  body {
    font-family: arial, sans-serif;
  }

  #headerDiv {
    padding: 20px;
    margin-bottom: 20px;
    background-color: #B5EDBC;
    color: #006633;
    font-size: larger;
    font-weight: bold;
  }

  .subTitle {
    font-size: smaller;
    float: center;
  }

  .gadgets-gadget-chrome {
    width: 60%;
    float: none;
    margin: auto;
  }

  .gadgets-gadget {
    width: 100%;
  }

 
 
 
 
 
 
 
 

 


 var parentUrl = document.location.href;
 var baseUrl = parentUrl.substring(0, parentUrl.indexOf('socialnotescontacts.html'))

 // Note: This is mapped to the socialdata servlet and is assumed to be running locally.
 // Edit accordingly...
 var socialDataPath = document.location.protocol + "//" + document.location.host
    + "/gadgets/socialdata";

 var gadgetUrl = baseUrl + 'examples/SocialNotesContacts.xml';
 var gadget;
 // 1。 預設定義了一個使用者 id,對應地址簿中一個文件的  UNID。
 var viewerId = "64ACA5F4F5B7F8B94825766B0020A0D9";
 var wnerId = "64ACA5F4F5B7F8B94825766B0020A0D9";

 function initGadget() {

  // Render gadget
  document.getElementById("gadgetUrl").value = gadgetUrl;

  gadget = gadgets.container.createGadget({'specUrl': gadgetUrl});;
  gadget.setServerBase('../../');

  // Viewer and Owner
  document.getElementById("viewerId").value = viewerId;
  document.getElementById("ownerId").value = ownerId;
  gadget.secureToken = escape(generateSecureToken());

  gadgets.container.addGadget(gadget);
  gadgets.container.layoutManager.setGadgetChromeIds(['gadget-chrome']);
  gadgets.container.renderGadgets();
 };

 function generateSecureToken() {
  var appId = 0;
  for (var i=0; i < gadgetUrl.length; i++) {
    appId += gadgetUrl.charCodeAt(i);
  }
  var fields = [ownerId, viewerId, appId, "shindig", gadgetUrl, "0","default"];
  for (var i=0; i < fields.length; i++) {
    // escape each field individually, for metachars in URL
    fields[i] = escape(fields[i]);
  }
  return fields.join(":");
 }


 SampleContainerGadget = function(opt_params) {
  gadgets.IfrGadget.call(this, opt_params);
 };

 SampleContainerGadget.inherits(gadgets.IfrGadget);
 gadgets.container.gadgetClass = SampleContainerGadget;


 
 
 
  
A Really Simple Gadget Testing Container

Gadget Url:
Viewer id: Owner id:


圖 5. Gadget 執行效果圖
圖 5. Gadget 執行效果圖

本例項中使用了 Lotus Notes API 訪問 Domino 地址簿,因此需要上傳 Notes.jar(位於安裝目錄 jvm\lib\ext 下),具體操作為:

  • 執行以下命令上傳 Notes.jar
    mvn install-file 
      -Dfile=C:\lotus\Notes\jvm\lib\ext\notes.jar 
      -DgroupId=notes 
      -DartifactId=notes 
      -Dversion=8.0 
      -Dpackaging=jar
    

  • 修改 pom.xml,在 dependency 中新增:
    
        notes
        notes
        8.0
    
    

另外,如果測試機沒有安裝 Lotus Notes,那麼讀者需要上傳相關的動態連結庫,例如 nlsxbe.dll、nxmlproc.dll 等等。

 

結束語

通過本文,讀者已經瞭解瞭如何使用 Shindig SPI 來將自己的 Social 資料適配到 Shindig 平臺,也瞭解瞭如何構建客戶端的應用來消費這些 Social 資料。

現在,我們不妨回頭總結一下整個 OpenSocial 平臺的系統結構。一般來說,OpenSocial 系統應用使用 OpenSocial   Gadget 作為應用前端,OpenSocial Gadget 類似於 iGoogle Gadget,不過增加了 OpenSocial 資料的訪問能力。當然,你也可以使用基於 REST/RPC 協議構建的桌面 RCP 應用作為前端。而對於 OpenSocial 平臺伺服器端,也有兩個選擇,一則如本文所討論的這樣,利用成熟開源的,與 OpenSocial 規範相相容的 OpenSocial 容器實現 ( 例如 Shindig),通過 SPI 擴充套件實現自己的 OpenSocial 容器,或者你從頭開始實現 OpenSocial 規範相相容的 OpenSocial 容器。

原文連結:http://www.ibm.com/developerworks/cn/lotus/nd-opensocial/index.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/14751907/viewspace-674789/,如需轉載,請註明出處,否則將追究法律責任。

相關文章