Lotus Notes/Domino 的 C API 编程
使用 Lotus C API for Lotus Notes/Domino 常常使人想起一把方便好用的老式瑞士军刀:一个时髦的小工具箱,包含许多没有书面文档的(undocumented)有用部件!本文旨在重点介绍 Lotus C API for Lotus Notes/Domino 的一些功能,帮助开发人员重新发现其无限的潜在用途。对 Lotus Notes/Domino 有基本的了解并熟悉编程语言就足以应付本文的内容。C 编程语言的应用知识可以帮助您更好地理解我们讨论的一些概念。
可以通过访问 Toolkits & Drivers page 下载 Lotus C API toolkit for Lotus Notes/Domino。本文的目的是想将该工具箱用于 Windows 平台上的 Lotus Notes/Domino 6.5。在将下载的档案文件中的这些文件解压缩之后,就可以获得文档、头文件、库文件、已编译的 OBJ 文件、示例程序和示例中使用的数据库。
文档中包括一个用户指导和一个参考指导,它们都是以单独 Notes 数据库的形式出现的。您可以从文档中获得许多信息。同时,用户指导中包含可以使用该工具箱完成哪些任务以及如何完成这些任务的介绍,而参考指导记录了所有可用功能。请仔细考虑一下这些数据库中包含的大量信息,这对为数据库创建全文(full-text)索引很有用,这样就可以快速准确地进行查找。
头文件通常可以在 Include 文件夹中找到。它们包含用于所有可作为该工具箱的一部分的常数、结构、宏、公共函数的定义。您需要根据程序中使用的 API 调用在源代码中包含相应的头文件。库文件和已编译的 OBJ 文件通常可以在不同操作系统的特定文件夹下的 Lib 文件夹中找到。因为 LIB 文件是需要链接到 API 程序的 DLL 导入库,所以 OBJ 文件是使用 NotesMain 入口点的程序或完成插件服务器任务所需的引导程序对象。(我们将在文章的后面部分再次回顾这些内容。)
示例文件夹中提供了一个详尽的示例程序列表。notedata 文件夹通常包含示例程序中用到的所有数据库。参考指导中描绘了每个函数或符号值,它们至少可以被一个示例程序引用,您可以查看该参考指导,以便了解如何在实际程序中使用即将检查的函数。
在明白了我们必须做什么之后,继续研究该工具箱的最好办法就是钻研一个实际的程序。在下一节中,我们将通过遍历两个不同的程序来研究该工具箱。
让我们从一个简单的程序开始:一个输出 Notes 数据目录的完全路径的程序。首先,让程序中所需的 C 库中包含头文件:
#include <stdio.h> #include <string.h>
接下来,添加来自 Lotus C API for Lotus Notes/Domino 的头文件:
#include <global.h> #include <osfile.h>
现在,主函数的任务是调用 NotesInitExtended() 函数来初始化 Notes 运行库。除非使用的是 NotesMain() 函数而不是 main() 函数,否则必须显式调用 NotesInitExtended() 函数:
int main(int argc, char *argv[]) { char DataDir[256]; STATUS error = NOERROR; WORD wlength; if (error = NotesInitExtended (argc, argv)) { printf("/n Unable to initialize Notes./n"); return (1); } |
最后,我们获得数据目录并输出它。OSGetDataDirectory() 是这个程序中使用的主要 Lotus C API 函数。顾名思义,OSGetDataDirectory() 包含数据目录的完全路径。NotesTerm() 函数关闭了 Notes 运行库,以结束函数的运行。只有在使用 NotesInitExtended() 启动运行库的时候,我们才必须显式调用该函数:
wlength = OSGetDataDirectory(DataDir); if (wlength > 0) printf("/n The data directory is %s", DataDir); NotesTerm(); return (0); } |
接下来,我们要编译和链接刚才编写的程序。为此,需要确保已正确地建立了使用环境。当然,最重要的要求是安装 Lotus Notes 和 Lotus C API toolkit for Lotus Notes/Domino 的匹配版本。此外,还需要 Microsoft Visual C++ 开发环境,以及随该环境一起提供的 Microsoft C/C++ 编译器和库。最后,我们需要设置三个环境变量:
- PATH 变量值应该包含 Notes 程序目录和 Microsoft C Compiler 所在的目录。
- INCLUDE 变量应该包含 Lotus C API 安装所需的 Include 目录和 Microsoft C Include 目录。
- LIB 变量应该包含 Lotus C API 安装所需的用于 Windows 32 平台的 Lib 目录和 Microsoft C Lib 目录。
如果您喜欢动态地安装环境变量,那么可以使用批处理文件。以下是一个示例批处理文件:
@echo off rem *** Comments: rem *** C:/Lotus/Notes is the program directory for Lotus Notes 6.5; rem *** C:/Program Files/Microsoft Visual Studio/VC98/ is the rem *** directory where Microsoft Visual C++ is installed; and rem *** c:/notesapi is the directory where the Lotus C API for rem *** Lotus Notes/Domino 6.5 is installed. set Path=.;C:/Program Files/Microsoft Visual Studio/VC98/bin; C:/Program Files/Microsoft Visual Studio/VC98/../Common/MSDEV98/bin; C:/Program Files/Microsoft Visual Studio/VC98/../Common/Tools;c:/Lotus/Notes set LIB=C:/Program Files/Microsoft Visual Studio/VC98/lib; C:/Program Files/Microsoft Visual Studio/VC98/Platformsdk/Lib; C:/Program Files/Microsoft Visual Studio/VC98/mfc/lib;C:/notesapi/lib/mswin32 Set INCLUDE=.;C:/Program Files/Microsoft Visual Studio/VC98/PlatformSDK/include; C:/Program Files/Microsoft Visual Studio/VC98/include; C:/Program Files/Microsoft Visual Studio/VC98/atl/include; C:/Program Files/Microsoft Visual Studio/VC98/mfc/include;C:/notesapi/include |
在运行该批处理文件之后,也就准备好了用于编译和链接程序的环境。nmake 工具是 Microsoft Visual C++ 安装的一部分,在这里用起来非常得心应手。为该工具提供一个 MAK 文件,以便为您“制造”应用程序。MAK 文件指定可交付使用的产品、它们的从属性和构建这些可交付使用产品的命令(如果它们不存在或者比它们所从属的产品还要老的话)。让我们来调用已经构造好的程序 simple.c。以下是可以用来构建 simple.exe 的 MAK 文件的一个示例:
# Comment: # The MAK file for simple.c - a simple Notes API program !include <ntwin32.mak> # The name of our program. PROGNAME = simple # Deliverables and dependencies $(PROGNAME).EXE: $(PROGNAME).OBJ $(PROGNAME).OBJ: $(PROGNAME).C # Compile our program .C.OBJ: $(cc) $(cdebug) $(cflags) $(cpuflags) /DNT $(cvars) $*.c # Link our program .OBJ.EXE: $(link) $(linkdebug) $(conflags) -out:$@ $** $(conlibs) / notes.lib user32.lib |
为了检查可与 nmake 工具一起使用的选项,可以使用命令 nmake -help。用来构建输出数据目录的 simple.exe 程序的命令如下所示:
nmake /f simple.mak /a
其中 simple.mak 是我们前面创建的 MAK 文件,/a 选项指出了我们想构建的所有东西:
可以使用 NotesMain() 函数作为入口点来编写相同的程序。惟一不同的是,我们不必像在前面的例子中那样调用 NotesInitExtended() 和 NotesTerm()。以下就是这个函数:
/* Simple program to find the data directory using the Lotus C API for Lotus Notes/Domino Using NotesMain() as the entry point*/ /* Include header files that we need from the C library */ #include <stdio.h> #include <string.h> /* Include header files that we need from the Lotus C API for Lotus Notes/Domino */ #include <global.h> #include <osfile.h> /* Functions defined in this file */ void APIErrHandler (STATUS); /* The NotesMain() function */ STATUS LNPUBLIC NotesMain(int argc, char far *argv[]) { /* Local variables */ char DataDir[256]; /* The data directory for Lotus Notes*/ STATUS error = NOERROR; /* Return type for most Lotus C API functions for Lotus Notes/Domino - defined in global.h */ WORD wlength; /* Unsigned integer - defined in global.h */ /* Get the full path of the data directory which is returned by the OSGetDataDirectory() function in the text buffer whose address is passed in as the argument. The function return value is the length of the buffer returned. */ wlength = OSGetDataDirectory(DataDir); /* Print the data directory path. */ if (wlength > 0) printf("/n The data directory is %s", DataDir); return (NOERROR); } |
如果您认为该函数改变了 MAK 文件,那么您是对的。还记得我们前面谈过的引导程序对象吗?在这里,我们需要使用这个对象。因此,MAK 文件使引导程序变成以下这样:
# Comment: # The MAK file for simplever2.c - a simple Notes API program that uses NotesMain() !include <ntwin32.mak> # The name of our program PROGNAME = simplever2 # Deliverables and dependencies $(PROGNAME).EXE: $(PROGNAME).OBJ $(PROGNAME).OBJ: $(PROGNAME).C # Compile our program .C.OBJ: $(cc) $(cdebug) $(cflags) $(cpuflags) /DNT $(cvars) $*.c # Link our program (notice the bootstrap object) .OBJ.EXE: $(link) $(linkdebug) $(conflags) -out:$@ $** notes0.obj $(conlibs) / notes.lib user32.lib |
让我们转移到稍大一点的一个程序上来,该程序使用了比简单例子中更多的 Lotus C API。该程序将查找本地地址簿中的一个名称,如果该名称是有效的,则返回办公室电话和地址。这听起来是不是很有趣?与平时一样,我们将从程序中需要的 C 库的头文件开始,然后是来自 Lotus C API 的头文件:
#include <stdio.h> #include <string.h> #include <global.h> #include <nsfdb.h> #include <nif.h> #include <osmem.h> #include <miscerr.h> #include <osmisc.h> |
在这个程序中,我们添加了另一个用于错误处理的函数。在这里,要声明其原型并从 main() 函数开始。当然,在 main() 函数中,是从声明所需的所有本地变量开始的。要在构建程序时查明每个变量的用途。接下来要初始化 Notes 运行库:
void APIErrHandler (STATUS); int main(int argc, char *argv[]) { char *dbname = "names.nsf"; char *viewname = "($users)"; char firstname[256] = ""; char lastname[256] = ""; char key[256]; DBHANDLE dbhandle; NOTEHANDLE notehandle; NOTEID viewid; HCOLLECTION collhandle; COLLECTIONPOSITION collpos; HANDLE bufferhandle; NOTEID *nid; DWORD count; DWORD matches; DWORD whichnote = 0; STATUS error = NOERROR; WORD flg; BOOL found; char *itemname = ""; char itemvalue[256]; WORD itemlen; if (error = NotesInitExtended (argc, argv)) { printf("/n Unable to initialize Notes./n"); return (1); } |
我们需要用于该程序的命令行,以便查找 <firstname> <lastname>,因此,要确保包含所期望的大量命令行参数:
if (argc != 3) { printf("The syntax is: lookup <firstname> <lastname>"); NotesTerm(); return (1); } else { strcpy(firstname, argv[1]); strcpy(lastname, argv[2]); strcpy(key, firstname); strcat(key, " "); strcat(key, lastname); printf("/nContact information for %s :", key); printf("/n--------------------------------------------"); } |
接下来,需要打开地址簿,并对它进行处理。NSFDbOpen() 函数如下所示:
if (error = NSFDbOpen (dbname, &dbhandle)) { APIErrHandler (error); NotesTerm(); return (1); } |
在处理数据库之后,需要使用 NIFOpenCollection() 函数获得视图($users)的设计记录。NIFOpenCollection() 函数根据视图记录处理文档集。如果在这两个操作中的任何一个操作中出现错误,则需要关闭数据库并退出:
if (error = NIFFindView (dbhandle, viewname, &viewid)) { NSFDbClose (dbhandle); APIErrHandler (error); NotesTerm(); return (1); } if (error = NIFOpenCollection( dbhandle, dbhandle, viewid, 0, NULLHANDLE, &collhandle, NULLHANDLE, NULL, NULLHANDLE, NULLHANDLE)) { NSFDbClose (dbhandle); APIErrHandler (error); NotesTerm(); return (1); } |
现在我们需要查找想要在集合中查看的名称。NIFOpenCollection() 根据主要分类键来搜索集合,该键是视图的第一个列。(必须对视图进行分类。)如果找到该名称,则将它放在指向接受检查的 COLLECTIONPOSITION 的指针之前。如果该函数在运行期间出错,则意味着该名称不存在,或者出现了其他一些错误:
error = NIFFindByName ( collhandle, key, FIND_CASE_INSENSITIVE | FIND_FIRST_EQUAL, &collpos, &matches); if (ERR(error) == ERR_NOT_FOUND) { printf ("/nNo such name in the address book./n"); NIFCloseCollection (collhandle); NSFDbClose (dbhandle); NotesTerm(); return (0); } if (error) { NIFCloseCollection (collhandle); NSFDbClose (dbhandle); APIErrHandler (error); NotesTerm(); return (1); } |
通过所获得的 COLLECTIONPOSITION,可以调用 NIFReadEntries() 函数得到我们感兴趣文档的 NoteID。该函数将所需信息放置在 bufferhandle 中,我们将允许其指针进入。此外,我们将忽略一些上下文中不需要的参数(比如 count 和 flg)。然而,如果期望在缓冲区中返回的信息在大小上并不是微不足道的,那么需要在一个循环中封闭对 NIFReadEntries() 的调用,并测试 flg 参数的值。如果有太多的数据而无法将它们都放入缓冲区中,那么要在 flg 参数中设置 SIGNAL_MORE_TO_DO 位。如果在函数返回之后,缓冲区为 NULL,则需要退出:
if (error = NIFReadEntries( collhandle, &collpos, (WORD) (NAVIGATE_CURRENT), 0L, NAVIGATE_NEXT, matches - whichnote, READ_MASK_NOTEID, &bufferhandle, NULL, NULL, &count, &flg)) { NIFCloseCollection (collhandle); NSFDbClose (dbhandle); APIErrHandler (error); NotesTerm(); return (1); } if (bufferhandle == NULLHANDLE) { NIFCloseCollection (collhandle); NSFDbClose (dbhandle); printf ("/nEmpty buffer returned by NIFReadEntries./n"); NotesTerm(); return (0); } |
既然已经获得了我们想要获得的记录的 NoteID,接下来就可以打开它。首先,需要使用 OSLockObject() 函数锁定内存中的缓冲区并获得其地址。我可以将 NoteID 强制转换为 NOTEID:
nid = (NOTEID *) OSLockObject (bufferhandle); if (error = NSFNoteOpenExt( dbhandle, nid[0], 0, ¬ehandle)) { OSUnlockObject (bufferhandle); OSMemFree (bufferhandle); NIFCloseCollection (collhandle); NSFDbClose (dbhandle); APIErrHandler (error); NotesTerm(); return (1); } |
差不多完成了!现在,我们所需要做的一切就是检查地址和电话号码是否存在,如果存在,则输出它们。可以用 NSFItemIsPresent() 检查这些项是否存在,并用 NSFItemGetText() 获得这些项的值:
found = FALSE; itemname = "MailAddress"; found = NSFItemIsPresent (notehandle, itemname, (WORD)strlen (itemname)); if (found) { itemlen = NSFItemGetText ( notehandle, itemname, itemvalue, sizeof (itemvalue)); printf("/nMail Address: %s", itemvalue); } else { printf("/nNo Mail Address found"); } found = FALSE; itemname = "OfficePhoneNumber"; found = NSFItemIsPresent (notehandle, itemname, (WORD)strlen (itemname)); if (found) { itemlen = NSFItemGetText ( notehandle, itemname, itemvalue, sizeof (itemvalue)); printf("/nOffice Phone Number: %s", itemvalue); } else { printf("/nNo Office Phone Number found"); } |
通过解除缓冲区的锁定并释放其内存,然后关闭记录、集合和数据库,来结束函数的运行:
OSUnlockObject (bufferhandle); OSMemFree (bufferhandle); if (error = NSFNoteClose (notehandle)) { NIFCloseCollection(collhandle); NSFDbClose (dbhandle); APIErrHandler (error); NotesTerm(); return (1); } if (error = NIFCloseCollection(collhandle)) { NSFDbClose (dbhandle); APIErrHandler (error); NotesTerm(); return (1); } if (error = NSFDbClose (dbhandle)) { APIErrHandler (error); NotesTerm(); return (1); } NotesTerm(); return (0); } |
最后一道难题是 APIErrHandler() 函数。该函数获得与传递的错误有关的字符串,并输出这些字符串。
void APIErrHandler (STATUS error) { STATUS errorid = ERR(error); char errorstring[200]; WORD len; len = OSLoadString (NULLHANDLE, errorid, errorstring, sizeof(errorstring)); printf ("Encountered this error : %s", errorstring); } |
您可能已经注意到以 Lotus C API 函数命名的模式。所有名称以 NSF 开头的函数都必须使用数据库、记录或项。而那些名称以 NIF 开头的函数通常用来处理视图和集合。处理操作系统级信息(比如说内存中的锁定对象)的函数名是以 OS 开头的。在查找与想归档的信息匹配的函数时,这种命名约定非常有用。
可以从 Sandbox 下载这一节中描述的完整程序。
Lotus C API 在客户端和服务器端的一些上下文中都很有用。在这一节中,我们将重点介绍一些可用的选项。
通过使用 Lotus C API,就有可能创建并操纵诸如代理、表单、视图和导航器之类的设计元素。在创建设计元素时,NOTE_CLASS_xxx 值很重要,因为它会识别我们将创建的记录的类型。NOTE_CLASS_DOCUMENT 用于文档,NOTE_CLASS_FORM 用于表单,NOTE_CLASS_VIEW 用于视图,NOTE_CLASS_FILTER 用于代理和宏,等等。例如,您可以使用以下函数调用来创建一个视图设计记录:
WORD ClassView = NOTE_CLASS_VIEW; NSFNoteCreate(hDB, &hNote); NSFNoteSetInfo(hNote, _NOTE_CLASS, &ClassView);
您甚至可以使用 NSFFormulaCompile() 和 NSFComputeEvaluate() 函数编译和评估公式。
可以创建一个在服务器上运行的服务器任务,就像创建其他任务那样。尽管可以让插件任务(add-in task)执行某一项操作并退出,但插件任务通常用于需要定期执行的操作。Lotus C API 提供了一些用来构建插件任务的特定函数。该程序的主要入口点是 AddInMain() 函数。函数 AddInIdle() 用于控制程序中的主循环。
AddInDayHasElapsed()、AddInMinutesHaveElapsed() 和 AddInSecondsHaveElapsed() 可以帮助用户判断是否是时候执行定期的操作。可以使用负载 <taskname> 手工从服务控制台启动和停止插件任务,并告诉 <taskname> 退出该命令。通过在服务器的 Notes.ini 文件上的 ServerTasks 变量值中包含程序名,还可以将插件任务设置为随服务器的开启而自动开始,随服务器的关闭而关闭。
您可以将自己的操作添加到 Notes 客户机的菜单选项 Actions 中。为了做到这一点,入口点必须是一个具有以下格式的函数:
NAMRESULT LNCALLBACK FunctionName (WORD wMsg, LONG lParam)
该函数可以拥有任何名称,但必须在模块定义(DEF)文件的 EXPORTS 函数中用序数值 1 来声明它。第一个参数指出将执行的操作,第二个参数是特定于操作的信息。这项操作可以拥有以下这些值:
NAMM_INIT NAMM_INITMENU NAMM_COMMAND NAMM_TERM |
NAMM_INIT 指出现在可以添加一个 Action 菜单项。NAMM_INITMENU 允许程序修改菜单项,比如说允许使用或禁用的那些菜单项。NAMM_COMMAND 指出 Action 菜单项已经选定,而且现在您可以执行相关的操作。NAMM_TERM 提供了一个执行结束操作(比如释放内存)的机会。
Lotus C API 提供了执行日历和调度操作的能力,比如说创建日历条目或查找业务繁忙时段。创建日历条目(比如会议、约会、备忘录和全天的事件)涉及到使用 NSFNoteCreate() 函数在所需的邮件数据库中创建一条记录,并将所需的日历项添加到该记录中。
可以使用 SchRetrieve() 函数来检索某一用户特定时间的时间表。在检索时间表时,可以使用 SchContainer_GetFirstSchedule() 函数来获得第一个时间表对象。然后使用 Schedule_ExtractFreeTimeRange() 函数检索时间表对象范围内的空闲时间,或者使用 Schedule_ExtractBusyTimeRange() 函数获得时间表对象范围内的繁忙时间。
以下是 Lotus C API 提供的值得特别关注的一项服务:Extension Manager。它允许您在通过注册回调例程来执行某些 Notes 或 Domino 操作之前或之后运行定制进程。该程序的入口点应该是具有以下格式的一个函数:
STATUS LNPUBLIC FunctionName(void)
该函数可以拥有任何名称,但必须在模块定义(DEF)文件的 EXPORTS 函数中用序数值 1 来声明它。回调函数必须具有以下格式:
STATUS LNPUBLIC FunctionName(EMRECORD FAR * pExRecord);
在注册回调例程之前,使用 EMCreateRecursionID() 函数获得一个递归 ID 很有用。如果已经调用扩展,那么这样做可以防止相同的扩展再次被调用,所以我们推荐您这样做。EMRegister() 函数用于注册回调例程。这可以用一个例子来很好地说明:
EMRegister ( EM_NSFDBCLOSE, EM_REG_BEFORE, (EMHANDLER)gHandlerProc, gRecursionID, &hHandler); |
第一个参数是 EM_NSFDBCLOSE,它将识别我们想注册回调的操作。第二个参数是 EM_REG_BEFORE,它指出我们想在调用操作之前运行该程序。因此,只要调用 NSFDBClose() 操作,就会调用该程序,NSFDBClose() 操作用于关闭数据库。有许多可以为其注册回调例程的操作,这个参数值指出了每个以 EM_ 开头的操作(完整列表可以从 API 参考指导中获得)。第三个参数是想要调用的定制函数。第四个参数是递归 ID(如果有的话),最后一个参数是由取消注册时需要使用的函数返回的句柄。
在结束该程序时,要使用 EMDeregister() 函数取消对回调例程的注册。要指定构建到 Lotus Notes/Domino 的扩展,则需要使用服务器或客户机的 Notes.ini 文件中的 EXTMGR_ADDINS 变量(这取决于运行该扩展的位置)。
我们的示例代码之一展示了如何通过使用结束特定邮件文件的日历配置文件更新的例程来使用 Extension Manager。该程序为 NSFNoteUpdateExtended() 注册了一个回调例程。在这个回调例程中,我们将查看即将更新的记录是否是我们感兴趣的邮件文件,以及该记录是否是一个日历配置文件。如果是,则将日期和时间记录到一个日志文件中。在编译并链接该程序之后,需要使用以下格式在服务器的 Notes.ini 文件中列出它:
EXTMGR_ADDINS=<dllname>
可以从 Sandbox 中下载该程序的完整代码(包括 MAK 文件和模块定义文件)。