Takenoff Labs

Lotus Notes/Domino に関する Tips や、クラシックの名曲などを紹介します

[Notes/Domino] 設計解析講座: リッチテキストアイテム解析の基礎

リッチテキストアイテムは、ノーツの最も特徴的なアイテムです。文書のみならず、設計の様々なところでも使用されていますので、これが解析できるようになれば、ノーツの理解がより深まると思います。

リッチテキストとは

リッチテキストは、簡単に一言で言うと「複数の構造体が集まったもの」です。リッチテキストというと何か難しそうなイメージがあるかもしれませんが、実は仕組み自体はとってもシンプルで、構造体の「集まり方」に多少の規則があるに過ぎません。

リッチテキストの中にある構造体は、ノーツでは「CDレコード」と呼びます。CDレコードは、すべて「CD~」で始まる名前となっています。

CD とは「Compound Document」あるいは「Composite Data」の略で、日本語的には「複合ドキュメント」「複合データ」となります。ここからも、構造体が「複合」されたデータであることがわかるかと思います。

リッチテキスト形式が使用されている箇所

リッチテキスト形式が使われているアイテムには、以下のようなものがあります。$BODY、$ACTIONS が特に重要ですね。

  • フォーム・サブフォーム・ページのデータ($BODY、$HTMLCode、$Info)
  • アクションバー・ボタン($ACTIONS、$V5ACTIONS)
  • フレームセットのデータ($Frameset)
  • エージェントアクション、検索条件($AssistAction、$AssistQuery)
  • ナビゲータのデータ($ViewMapDataset、$ViewMapLayout、$NavImagemap)

注意したいのは、同じリッチテキスト形式のものでも、アイテム種類によって構造体の種類が違う場合があることです。たとえば、$BODY(TYPE_COMPOSITE)では、シグニチャ(IDみたいなもの)が 129 の構造体は CDPARAGRAPH ですが、$AssistQuery(TYPE_QUERY)では CDQUERYHEADER になります。

リッチテキスト形式が使われているアイテムは、以下の4種類に分類されます。この種類ごとに構造体が違うということを留意してください。

  • TYPE_COMPOSITE(1)
  • TYPE_QUERY(15)
  • TYPE_ACTION(16)
  • TYPE_VIEWMAP_DATASET(18)、TYPE_VIEWMAP_LAYOUT(19)

コールバック関数が使える言語の場合

C、C++、VB など、コールバック関数が使える言語の場合、EnumCompositeBuffer 関数を使うと、簡単にCDレコードを切り出してくれるようです。ちょうど SAX パーサのように、CDレコードが見つかるたびにコールバック関数を呼び出すのではないかと思います。(まあ、管理人は使ったことが無いので、想像ですが。)

しかし、LotusScript の場合、コールバック関数が使えないため、この関数を使うことができません……(x_x)。LotusScript ってこういうところがイケてないんですよね。せめて VB6 相当の機能はすべて入れて欲しいものです。

とまあ、愚痴っても仕方ないので、別の方法を考えましょう。

リッチテキストの構造

リッチテキストを LotusScript で読み解くには、その詳細な構造を知る必要があります。リッチテキストの構造については、APIドキュメントの「Chapter 7-1. Introduction to Rich Text」の「Low-Level Structure of Rich Text」に詳述されています。かいつまんで説明すると、

  1. リッチテキストは、構造体(CDレコード)が集まったものである。
  2. リッチテキストは、同名のアイテムが複数個になる場合がある。
  3. 1つのアイテムは最大64KBまでだが、概ね40KBくらいにすることが推奨される。
  4. CDレコードには先頭にヘッダがあり、ヘッダには BSIG、WSIG、LSIG の3タイプがある。
  5. CDレコードは、必ず偶数アドレスから始まる。

というような感じです。1~3 は、これまで説明してきたことですね。重要なのは、4 と 5 です。

ヘッダについては、ods.h のコメントを見るとわかりやすいです。

     0         1
+---------+---------+
|   Sig   |  Length | Byte signature(BSIG)
+---------+---------+
     0         1         2         3
+---------+---------+---------+---------+
|   Sig   |   ff    |      Length       | Word signature(WSIG)
+---------+---------+---------+---------+
     0         1         2         3         4         5
+---------+---------+---------+---------+---------+---------+
|   Sig   |   00    |                 Length                | DWord signature(LSIG)
+---------+---------+---------+---------+---------+---------+

つまり、

  • 先頭の1バイトは、シグニチャ(IDみたいのもの)であり、ここを見ればCDレコードの種類がわかる。
  • 2バイト目が「FF」なら、3~4 バイト目の値がそのCDレコードの長さである。
  • 2バイト目が「00」なら、3~6 バイト目の値がそのCDレコードの長さである。
  • 2バイト目が「FF」でも「00」でもなければ、2バイト目がそのCDレコードの長さである。

というわけです。したがって、解析プログラムでは、以下 1~3 をアイテムの長さ分だけ繰り返せばよいのです。

  1. CDレコードのヘッダからシグニチャを取得し、読み取りたいCDレコードのシグニチャであれば、解析処理を行う。
  2. CDレコードのヘッダから、CDレコードの長さを取得し(長さの取得は上記参照)、長さ分アドレスをインクリメントする。
  3. インクリメントした結果が奇数アドレスなら、アドレスに1を加算する。

サンプル: アクションボタン名の解析

では、いよいよ実際のコードです。ここでは例として、デフォルトビューのアクションボタン名を取得する処理を書いてみます。

Const WORDLEN = 65534

Const OS_TRANSLATE_NATIVE_TO_LMBCS = 0
Const OS_TRANSLATE_LMBCS_TO_NATIVE = 1

Const NOTESDLL = "nnotes.dll"

Const SIG_CD_ACTION = 190

Type BLOCKID
    hPool As Long
    Block As Integer
End Type

Declare Function NSFItemInfo Lib NOTESDLL (Byval hNote As Long, Byval ItemName As Any, _
Byval NameLen As Integer, ItemBlockid As BLOCKID, ValueDatatype As Integer, _
ValueBlockid As BLOCKID, ValueLen As Long) As Integer

Declare Function NSFItemInfoNext Lib NOTESDLL (Byval hNote As Long, Byval hPool As Long, _
Byval Block As Integer, Byval ItemName As Any, Byval NameLen As Integer, ItemBlockid As BLOCKID, _
ValueDatatype As Integer, ValueBlockid As BLOCKID, ValueLen As Long) As Integer

Declare Function OSLockObject Lib NOTESDLL (Byval hHandle As Long) As Long

Declare Function OSUnlockObject Lib NOTESDLL (Byval hHandle As Long) As Integer

Declare Function OSTranslate Lib NOTESDLL (Byval TranslateMode As Integer, _
Byval InData As String, Byval InLength As Long, Byval OutData As String, _
Byval OutLength As Long) As Integer

Declare Function OSLoadString Lib NOTESDLL (Byval hModule As Long, Byval StringCode As Long, _
Byval retBuffer As String, Byval BufferLength As Integer) As Integer

Declare Sub RtlMoveMemoryString Lib "kernel32" Alias "RtlMoveMemory" (Byval hpvDest As String, _
Byval hpvSource As Long, Byval cbCopy As Long)

Declare Sub RtlMoveMemory Lib "kernel32" Alias "RtlMoveMemory" (hpvDest As Any, Byval hpvSource As Long, _
Byval cbCopy As Long)

Sub Initialize
    Dim ss  As New NotesSession
    Dim db  As NotesDatabase
    Dim doc As NotesDocument
    
    Dim ItemBlockid  As BLOCKID
    Dim ValueBlockid As BLOCKID
    Dim strItemName  As String
    Dim lngLen       As Long
    Dim lngAddr      As Long
    Dim lngCnt       As Long
    Dim lngCDLen     As Long
    Dim intDataType  As Integer
    Dim intRet       As Integer
    Dim intSig       As Integer
    Dim intFlg       As Integer
    
    Set db  = ss.CurrentDatabase
    Set doc = db.GetDocumentByID("FFFF0008")
    
    If doc Is Nothing Then
        Msgbox "文書が見つかりません", 0, "エラー"
        Exit Sub
    End If
    
    strItemName = "$ACTIONS"
    
    intRet = NSFItemInfo(doc.handle, strItemName, Lenbp(strItemName), ItemBlockid, intDataType, ValueBlockid, lngLen)
    If intRet <> 0 Then
        Msgbox APIGetError(intRet), 0, "エラー"
        Exit Sub
    End If
    
    Do
        'メモリをロック
        lngAddr = OSLockObject(ValueBlockid.hPool) + Val("&B" & Right(Bin$(ValueBlockid.Block), 16) & "&")
        
        lngAddr = lngAddr + 2 '最初の2バイトはアイテムタイプなので読み飛ばす
        lngLen  = lngLen - 2  'アイテムタイプを除いた値のサイズ
        
        lngCnt = 0
        intFlg = 0
        Do
            Call RtlMoveMemory(intSig, lngAddr, 1)
            Call RtlMoveMemory(intFlg, lngAddr + 1, 1)
            
            Select Case intFlg
            Case &H00 'DWORD Signature
                If (lngCnt + 2 + 4) => lngLen Then Exit Do
                Call RtlMoveMemory(lngCDLen, lngAddr + 2, 4)
                
                '(↓ここに分析用の処理を記述)
                Select Case intSig
                Case SIG_CD_ACTION
                    Call ReadCDAction(lngAddr)
                End Select
                '(↑ここに分析用の処理を記述)
                
            Case &HFF 'WORD Signature
                If (lngCnt + 2 + 2) => lngLen Then Exit Do
                Call RtlMoveMemory(lngCDLen, lngAddr + 2, 2)
                
                '(↓ここに分析用の処理を記述)
                Select Case intSig
                Case SIG_CD_ACTION
                    Call ReadCDAction(lngAddr)
                End Select
                '(↑ここに分析用の処理を記述)
                
            Case Else 'BYTE Signature
                lngCDLen = intFlg
                
                '(↓ここに分析用の処理を記述)
                
                '(↑ここに分析用の処理を記述)
                
            End Select
            
            If intSig <= 0 Or lngCDLen <= 0 Then Exit Do '念のため
            
            'CDレコードのサイズ分足す
            lngAddr = lngAddr + lngCDLen
            lngCnt  = lngCnt  + lngCDLen
            
            '偶数アドレスにする
            lngAddr = lngAddr + (lngAddr And 1&)
            lngCnt  = lngCnt  + (lngCnt  And 1&)
            
        Loop While lngCnt < lngLen
        
        'メモリロックを解除
        Call OSUnlockObject(ValueBlockid.hPool)
        
        Loop While NSFItemInfoNext(doc.handle, ItemBlockid.hPool, ItemBlockid.Block, strItemName, Lenbp(strItemName), _
    ItemBlockid, intDataType, ValueBlockid, lngLen) = 0
End Sub

Function APITranslate(Byval intMode As Integer, Byval strBuf As String) As String
    Dim strOut As String
    
    strOut = Space$(WORDLEN)
    Call OSTranslate(intMode , strBuf, Lenbp(strBuf) - 1, strOut, WORDLEN - 1)
    APITranslate = Strleft(strOut & Chr(0), Chr(0))
End Function

Function APIGetError(Byval intErrCode As Integer) As String
    Dim strIn   As String
    Dim strOut  As String
    Dim intCode As Integer
    
    strIn  = Space$(255)
    strOut = Space$(255)
    
    intCode = intErrCode And &h3FFF
    Call OSLoadString(0, intCode, strIn, Lenbp(strIn) - 1)
    
    strOut = APITranslate(OS_TRANSLATE_LMBCS_TO_NATIVE, strIn)
    
    APIGetError = Trim(strOut)
End Function

Sub APIMoveMemory(varVal As Variant, lngAddr As Long, Byval lngLen As Long)
    Call RtlMoveMemory(varVal, lngAddr, lngLen)
    lngAddr = lngAddr + lngLen
End Sub

Sub APIMoveMemoryString(strVal As String, lngAddr As Long, Byval lngLen As Long)
    strVal = Space$(lngLen + 1)
    Call RtlMoveMemoryString(strVal, lngAddr, lngLen)
    lngAddr = lngAddr + lngLen
End Sub

Sub ReadCDAction(Byval lngAddr As Long)
    Dim strName As String
    Dim intSig As Integer
    Dim lngLen As Long
    Dim intType As Integer
    Dim intIconIndex As Integer
    Dim lngFlags As Long
    Dim lngTitleLen As Long
    Dim lngFormulaLen As Long
    Dim lngShareId As Long
    
    On Error Goto ERR_CASE
    
    Call APIMoveMemory(intSig, lngAddr, 2)
    Call APIMoveMemory(lngLen, lngAddr, 4)
    Call APIMoveMemory(intType, lngAddr, 2)
    Call APIMoveMemory(intIconIndex, lngAddr, 2)
    Call APIMoveMemory(lngFlags, lngAddr, 4)
    Call APIMoveMemory(lngTitleLen, lngAddr, 2)
    Call APIMoveMemory(lngFormulaLen, lngAddr, 2)
    Call APIMoveMemory(lngShareId, lngAddr, 4)
    
    'アクション名
    If lngTitleLen > 0 Then
        Call APIMoveMemoryString(strName, lngAddr, lngTitleLen)
        Msgbox APITranslate(OS_TRANSLATE_LMBCS_TO_NATIVE, strName)
    End If
    
    Exit Sub
    
ERR_CASE:
    Msgbox "Error - " & Getthreadinfo(1) & " Line:" & Cstr(Erl) & " Msg:" & Error$, 0, "Error"
    Exit Sub
End Sub

キモの部分は、「Do ~ Loop While lngCnt < lngLen」の部分です。ここでリッチテキストの規則どおりに読み取っているのがおわかりいただけるでしょうか。このループ内では、CDレコードが見つかるたびにSelect文が評価されますので、解析したいCDレコードのシグネチャ(intSeg)の値が来たら、CDレコード解析処理に入ってやればよいのです。

アクションボタンは、CDACTION という構造体で定義されます。CDACTION 構造体の定義は、以下のとおりです。

typedef struct {
  LSIG  Header;    /* Signature and Length */
  WORD  Type;      /* Type of action (formula, script, etc.) */
  WORD  IconIndex; /* Index into array of icons */
  DWORD Flags;     /* Action flags */
  WORD  TitleLen;  /* Length (in bytes) of action's title */
  WORD  FormulaLen;/* Length (in bytes) of "hide when" formula */
  DWORD ShareId;   /* Share ID of the Shared Action */
/* Variable portion of the record:
        TitleLen bytes of action's title
        Action data:  (Header.Length - TitleLen - FormulaLen) bytes
         of formula, script, etc.
        FormulaLen bytes of "hide when" formula */
} CDACTION;

上記のとおり、構造体の後ろに、可変長でアクション名、アクションデータ(式、スクリプトなど)、非表示式が続く構造になっています。これを ReadCDAction サブルーチンで読み取っています。(今回は処理を簡単にするために、アクション名だけを取得しています。)

解析処理自体は、前回と似てますね。CDレコードさえ切り出してやれば、あとは構造体の解析と何ら変わらないということがおわかりいただけるかと思います。

以上のようにすれば、リッチテキスト内のどんなデータでも解析することが可能です(実際にはいろいろと面倒なところもありますが)。LotusScript だけでリッチテキストから自由にデータを取れると、ちょっと楽しくなりませんか?

(おまけ)LSIG って……

賢明な方は「CDACTION のヘッダは LSIG(DWORD Signature)になっているのに、なんで WORD Signature (Case &HFF)のところにも処理が書いてあるの?」というところにお気づきになったかもしれません。これは、LSIG であっても、CDレコードのサイズが64KB(WORDのサイズ)以下であれば、WSIG 扱いになるためです(Length は4バイトで変わりません)。なので、ヘッダが LSIG であっても、WSIG 用の処理も書く必要があります。

ここで1つ疑問が発生します。「アイテムのサイズは基本64KBまでなのに、LSIG になることってあるの?」

……すみません、この疑問に対する回答は、管理人は持ちあわせていませんorz 試しに、64KBを超えるスクリプトを書いてみたりしたのですが、やっぱり LSIG にはならず、スクリプト用の別のアイテムができてしまい、Designer でアクションを選択しようとすると赤い画面で不正終了する始末(R6.5.4の場合)……。う~ん、ノーツは謎が多すぎる……。

1 Star2 Stars3 Stars4 Stars5 Stars (No Ratings Yet)
読み込み中...

Navigation

トラックバック

トラックバックはありません

コメント

コメントはありません

※コメントは承認制となっております。管理者が承認するまで表示されません。申し訳ありませんが、投稿が表示されるまでしばらくお待ちください。





(以下のタグが使えます)
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

For spam filtering purposes, please copy the number 4479 to the field below:

^
×