Lotus Notes/Domino 环境下的 OpenSocial 开发

刘 奇, 软件工程师, IBM
刘奇是 IBM 中国软件工程师,现在 IBM 中国软件开发实验室 Lotus Notes Client 开发中心工作。
李 三红, 高级软件工程师, IBM
李三红,任职于 IBM CDL,负责 Lotus Notes 产品研发。在这之前,作者一直从事网络应用开发相关工作。作者感兴趣的技术领域包括:分布式对象计算,网络应用,OSGi,协作计算,Java 安全等方面。

简介: 随着社交网络的不断发展,用户关系信息已经成为一类重要的网络数据。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 规范的引用实现,其主要的组件包括 :

  • Gadget Container JavaScript,OpenSocial Gadget 容器,客户端的 JavaScript. 类库 (gadget.js),提供例如 UI Layout,Security, Communication 等相关的功能。
  • Gadget Rendering Server,负责解析 Gadget XML, 转化成浏览器使用的 HTML/JavaScript/CSS。
  • OpenSocial Container JavaScript,位于客户端的 OpenSocial 容器,也是 JavaScript. 类库,提供 OpenSocial 相关的功能,例如存取 People, Activity, AppData 等相关的社交数据。
  • OpenSocial Data Server,提供基于 Restful/RPC 协议的 Services,用于存取 People, Activity, AppData 等相关的社交数据

关于更多的有关 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 实现

				
<?xml version="1.0" encoding="UTF-8"?>
 
     
             .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;
              }
         

         
List Contacts
Name E-Mail Phone Company
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 中添加:
    notesnotes8.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/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/14751907/viewspace-674789/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值