NSFItemScan関数の限界(その1) - Noteクラス - 「NotesPeek」をQtでリメイク

今回は、リッチテキストから少し離れて、文書からアイテム(フィールド)データを取得するプロセスについての考察をします。

Notes文書(Note)には、いろいろな種類のアイテムが格納されます。文書のプロパティやNotesPeekのように、文書内のアイテムをすべて読み込むためには、NSFItemScan関数を使います。

// nsfnote.h

typedef STATUS (LNCALLBACKPTR NSFITEMSCANPROC)(
    WORD Spare,
    WORD ItemFlags,
    char far *Name, WORD NameLength,
    void far *Value, DWORD ValueLength,
    void far *RoutineParameter);

STATUS LNPUBLIC NSFItemScan (
    NOTEHANDLE hNote,
    NSFITEMSCANPROC ActionRoutine,
    void far *RoutineParameter);

事前にNSFITEMSCANPROCのような引数を取るコールバック関数を用意しておき、アイテムごとにその関数を呼び出すことで、すべてのアイテムに対して処理をすることができます。なお、NSFItemScanではアイテムの変更処理はできないので、アイテムデータの更新をしたい場合は、NSFItemScan関数の呼び出しを終えてからということになります。

さて、タイトルにも書いたNSFItemScan関数の限界とは何か、その前置きとして、重複アイテムについて軽く触れておきます。

Notesの文書には通常、アイテムの名前は重複しない作りのように見えます。フォームの設計をしているときなどは当たり前の話ですが、技術的には同名アイテムが重複することは可能です。理由の1つに、1アイテム当たり64キロバイトの壁の突破があります。文書にアイテムを追加する関数に「NSFItemAppend 」というのがあります。

// nsfnote.h

STATUS LNPUBLIC NSFItemAppend (
    NOTEHANDLE hNote, WORD ItemFlags,
    const char far *Name, WORD NameLength,
    WORD DataType,
    const void far *Value, DWORD ValueLength);

ValueLengthという引数の型がDWORDなので、4GBまでいけるのではと錯覚を起こしそうですが、APIのリファレンスマニュアルには次のように明確に書かれています。

The length of the item’s value (excluding the data type word). The length must not exceed 64K bytes. If the ITEM_SUMMARY flag is set in item_flags, then value_len must not exceed 32K.

(訳) (ValueLengthは)項目の値の長さです(データタイプを表すWORD型を除く)。長さは64Kバイトを超えてはなりません。item_flagsにITEM_SUMMARYフラグが設定されている場合、value_len(ValueLength)は32Kを超えてはなりません。

ビューなどで使うためにサマリーフラグを立てている場合は、さらに32キロバイトを超えてはいけないのです。しかし、テキストデータは32kBや64kBを超えることは意外にあることだと思いますし、リッチテキストならなおのことです(文書プロパティでBodyアイテムがいくつもあるのを目の当たりにしたことがあると思います)。実は、この壁を越えるために名前重複アイテムが使われています。前出のNSFItemAppend関数は、文書内に同名アイテムがあると、同じ名前だけど特殊なIDを振って文書内に同名アイテムを保存します。なお、1つのアイテムデータをどこで区切るかは関数の呼び出し側に責任があるようです。Notesユーザーがこれを意識することなく使えているのは、Notesクライアントが適当なところで区切って保管してくれているようです。

もう1つ、同名アイテムが存在する理由に、添付ファイルがあります。添付ファイルの本体は「$FILE」というフィールドに保管されますが、添付ファイルがいくつもあると、その数分だけ$FILEアイテムが存在します。添付ファイルがどれほど大きくても、$FILEアイテム1つに1ファイルが保管されます(先程とはまるで違いますね)。ファイルの添付処理はファイルシステムから直接行われるので、呼び出し側は特にサイズを気にする必要はないわけです。

同名アイテムは、添付ファイルであれば1アイテムを1ファイルとして、それ以外であれば、分割されたデータを合成して64キロバイトの壁を越える手段として使われています。

前置きが長くなりましたが、本件のリメイクプロジェクトで文書内のアイテムを取得するに当たり、この同名アイテムの存在が、NSFItemScan関数にある不具合をもたらしていました。次回、その不具合と対処方法について考察していきます。

CDフォントテーブルを列挙 - リッチテキスト - 「NotesPeek」をQtでリメイク

前回、CD(Composite Data、リッチテキスト内の構成要素)のテキスト要素「CDTEXT」に含まれるフォントの修飾データを明示化しました。この中にはもちろんフォントの書体も含まれますが、5種類の書体定義しか存在しません。書体の種類は、パソコンの黎明期ならいざ知らず、現在ではあまたの書体が存在します。

Notesのリッチテキストは、これを専用のリッチテキスト形式フィールドに書体テーブルを作って、書体番号を振り、参照することで解決しています。書体番号は、0~4がデフォルトの書体定義で、5~255(推定)が書体番号になります。同一文書(Note)内の「$Fonts」リッチテキストフィールド内にあるCDFONTTABLEデータと、後続のCDFACE配列を参照すると、同じ書体番号が振られた書体データを取得することができることになります。

typedef struct {
  WSIG  Header;  /* Tag and length */
  WORD  Fonts;   /* Number of CDFACEs following */
} CDFONTTABLE;   /* Now come the CDFACE records... */

typedef struct {
  BYTE Face;    /* ID number of face */
  BYTE Family;  /* Font Family */
  char Name[MAXFACESIZE];
} CDFACE;

CDFONTTABLE::Fontsは、後続のCDFACE配列の数を表すので、CDFONTTABLEを取得した後にFonts分だけCDFACEデータを取得すればよいことになります。

CDFACE::Faceは、CDTEXT::FontID::Faceが参照する書体番号を表します。

CDFACE::Familyは、フォントファミリーを表します。ビットマスクで構成されていて、APIリファレンスによればWindowsの定義を用いているようです。以下に、関連する定義を抜粋しておきます。

/* EnumFonts Masks */
#define RASTER_FONTTYPE     0x0001
#define DEVICE_FONTTYPE     0x0002
#define TRUETYPE_FONTTYPE   0x0004

#define DEFAULT_PITCH           0
#define FIXED_PITCH             1
#define VARIABLE_PITCH          2
#if(WINVER >= 0x0400)
#define MONO_FONT               8
#endif /* WINVER >= 0x0400 */

/* Font Families */
#define FF_DONTCARE         (0<<4)  /* Don't care or don't know. */
#define FF_ROMAN            (1<<4)  /* Variable stroke width, serifed. */
                                    /* Times Roman, Century Schoolbook, etc. */
#define FF_SWISS            (2<<4)  /* Variable stroke width, sans-serifed. */
                                    /* Helvetica, Swiss, etc. */
#define FF_MODERN           (3<<4)  /* Constant stroke width, serifed or sans-serifed. */
                                    /* Pica, Elite, Courier, etc. */
#define FF_SCRIPT           (4<<4)  /* Cursive, etc. */
#define FF_DECORATIVE       (5<<4)  /* Old English, etc. */

素朴な疑問として、マルチプラットフォームであるNotesでは、MacLinuxではどのようになるのか、どこかで検証してみたいと思います。

CDFACE::Nameは、書体名を表します。書体名は最大32バイトで、これもWindows APIの影響を受けています。Shift-JISやUnicodeなら全角は2バイトで済み、16文字まで入りますが、Notesの文字列格納はLMBCS形式なので、全角や半角カナが3バイトになってしまい、すべて全角では10文字しか入りません。心配でなりません。夜も眠れません。

それはさておき、このCDFONTTABLEとCDFACE配列を、ntlx::cd::Baseテンプレートクラスから派生させたntlx::cd::FontTableクラスとして実装した例が以下になります。

// ntlx/cd/fonttable.h

#ifndef NTLX_CD_FONTTABLE_H
#define NTLX_CD_FONTTABLE_H

#include <ntlx/cd/base.h>
#include <QVariant>

#if defined(NT)
#pragma pack(push, 1)
#endif

#include <editods.h>

#if defined(NT)
#pragma pack(pop)
#endif

Q_DECLARE_METATYPE(CDFACE)

namespace ntlx
{
namespace cd
{

class NTLXSHARED_EXPORT FontTable
    : public Base<CDFONTTABLE, _CDFONTTABLE, SIG_CD_FONTTABLE>
{
public:
  FontTable(char** ppRecord);
  FontTable(const FontTable& other);
  FontTable& operator=(const FontTable& other);

  virtual QString toString() const;

private:
  QList<CDFACE> faceList_;
};

} // namespace cd

} // namespace ntlx

#endif // NTLX_CD_FONTTABLE_H
// cd/fonttable.cpp

#include "ntlx/cd/fonttable.h"
#include "ntlx/lmbcs.h"

namespace ntlx
{
namespace cd
{

FontTable::FontTable(char** ppRecord)
  : Base<CDFONTTABLE, _CDFONTTABLE, SIG_CD_FONTTABLE>(ppRecord)
  , faceList_()
{
  for (WORD i = 0; i < record_.Fonts; ++i)
  {
    CDFACE face;
    ODSReadMemory(ppRecord, _CDFACE, &face, 1);
    faceList_.append(face);
  }
}

FontTable::FontTable(const FontTable &other)
  : Base<CDFONTTABLE, _CDFONTTABLE, SIG_CD_FONTTABLE>(other)
  , faceList_(other.faceList_)
{
}

FontTable& FontTable::operator=(const FontTable& other)
{
  Base<CDFONTTABLE, _CDFONTTABLE, SIG_CD_FONTTABLE>::operator=(other);
  if (this != &other)
    faceList_ = other.faceList_;
  return *this;
}

QString FontTable::toString() const
{
  const QString templ("[%1:%2:%3]");
  QStringList result;
  for (auto it = faceList_.constBegin(); it != faceList_.constEnd(); ++it)
  {
    Lmbcs name((*it).Name);
    result.append(
          templ
          .arg((ushort)(*it).Face)
          .arg((ushort)(*it).Family)
          .arg(name.toQString())
          );
  }
  return result.join(",");
}

} // namespace cd

} // namespace ntlx

この実装を使って、NSFinderを拡張した例を見ていきます。サンプルの文書は、以下のようになります。

f:id:takahide-kondoh:20170911075751p:plain

この文書の$Fontsフィールドの、Font Tableを見ると以下のようになります。

【前半】

f:id:takahide-kondoh:20170911075803p:plain

【後半】

f:id:takahide-kondoh:20170911075815p:plain

デフォルトの書体定義を除くと、251種類までしか指定ができない書体の種類。これは多いのか、少ないのか。Notesが本格的な文書作成ツールではないという観点と、そんなに多くの書体が定義された文書は見るに堪えないという観点に立てば、十分すぎるかもしれないですね。ただ、余裕がないようにも見えてしまいます。APIではちょくちょくビット長の限界を見せられます。Notesのアーキテクチャは、バージョンを追うごとに拡張もされていますが、レガシーな部分も多く引きずっています。旧世代の仕様のままでも構いませんが、柔軟な構造を活用して、大胆な構造改革を起こすことも必要かもしれません。

CDテキストのフォント情報 - リッチテキスト - 「NotesPeek」をQtでリメイク

先週の体調不良が、嘘のように回復しました。また気を引き締めてNotesPeekをリメイクしていきます。

前回は、CDデータをテンプレートクラスベースで実装を簡略化し、その手始めとしてCDテキストを「cd::Textクラス」として実装してみました。今回は、CDテキストが持つもう1つの情報について掘り下げます。

CDテキストは、構造的には「CDTEXT構造体」と可変長の「文字列データ(LMBCS)」で構成されています。可変長の文字列データの長さは、CDTEXT::Header::Lengthが持つ全体の長さからODSLength(_CDTEXT)が返す長さを引けば算出できます。

CDTEXT構造体には、Headerの他にFONTID型のメンバを持っています。FONTID自体は単なるDWORD型ですが、同じバイト長を持つFONTIDFIELDSという構造体でも表せます。これは、FIDユニオンとして定義されています。

// #include <global.h>

typedef DWORD FONTID;
// #include <fontid.h>

/*  Font ID sub-fields */

typedef struct {
#ifdef LITTLE_ENDIAN_ORDER
  BYTE Face;       /* Font face (FONT_FACE_xxx) */
  BYTE Attrib;     /* Attributes (ISBOLD,etc - see below) */
  BYTE Color;      /* Color index (FONT_COLOR_xxx) */
  BYTE PointSize;  /* Size of font in points */
#else
  BYTE PointSize;  /* Size of font in points */
  BYTE Color;      /* Color index (FONT_COLOR_xxx) */
  BYTE Attrib;     /* Attributes (ISBOLD,etc - see below) */
  BYTE Face;       /* Font face (FONT_FACE_xxx) */
#endif
} FONTIDFIELDS;

/*  Font Union */

typedef union FID {
  FONTIDFIELDS x;
  FONTID FontID;
}FID;

以下のようなリッチテキストを例に、フォント属性について見ていきます。

f:id:takahide-kondoh:20170903124210p:plain

属性のないテキストは以下のようになります。

f:id:takahide-kondoh:20170903124310p:plain

Colorは0(黒)、Faceは1(標準ゴシック体)、Sizeは10ポイントになります。

FONTIDFIELDS::PointSizeはフォントのサイズを表します。BYTEは符号なしの8ビット長なので、理論上255ポイントまで表現が可能ということになります。

f:id:takahide-kondoh:20170903124520p:plain

Colorはフォント色を表します。サイズと同様8ビット長なので、256通りの色を表現できます。しかし、画面をよく見ると、Textの次にCDCOLOR、前後をCDBEGIN、CDENDで挟まれています。256色というのは過去の仕様で、現在フォント色はフルカラーを指定できるようになっています。いずれどこかで、CDCOLORレコードを含めて色について紐解いてみたいと思います。

f:id:takahide-kondoh:20170903124710p:plain

Faceはフォントの種類を表します。0~4番は既定のフォントタイプで、5以上は別に管理されているフォントテーブルを参照する形式を取ります。0~4番のフォントタイプは、Notesクライアントで見ると以下のフォントが該当します。

f:id:takahide-kondoh:20170903125304p:plain

標準的なフォントは「Default Sans Serif」(ゴシック系)で1番となります。

f:id:takahide-kondoh:20170903125139p:plain

明朝系の標準フォントは「Default Serif」で0番です。

f:id:takahide-kondoh:20170903125756p:plain

「デフォルトのUIフォント」は3番です。

f:id:takahide-kondoh:20170903125107p:plain

「デフォルトの等幅フォント」は4番です。

f:id:takahide-kondoh:20170903125953p:plain

「デフォルトのマルチリンガルフォント」は2番です。

f:id:takahide-kondoh:20170903130902p:plain

例えばこれを、「メイリオ」にすると、最初の定義外番号として5番が割り振られました。

f:id:takahide-kondoh:20170903130216p:plain

続いて「游ゴシック」にすると、6番が割り振られていました。

f:id:takahide-kondoh:20170903130338p:plain

今回説明は割愛しますが、デフォルトのフォント以外を設定すると、「$Fonts」というリッチテキストフィールドが生成されます。この中に可変長のフォントテーブルが構成されて、5番以降のフォントが管理されます。

それでは、残りのフォント属性について一部ですが見ていきます。

太字(Bold=True)

f:id:takahide-kondoh:20170903130037p:plain

斜体(Italic=True)

f:id:takahide-kondoh:20170903131014p:plain

取り消し線(StrikeOut=True)

f:id:takahide-kondoh:20170903131052p:plain

今回、このFONTID(FONTIDFIELDS)を管理するに当たり、cd::Textクラスのインナークラスとしてcd::Text::FontIdクラスを定義しました。以下はその実装例です。

// #include <ntlx/cd/text.h>

#ifndef NTLX_CD_TEXT_H
#define NTLX_CD_TEXT_H

#include <ntlx/cd/base.h>

#if defined(NT)
#pragma pack(push, 1)
#endif

#include <editods.h>

#if defined(NT)
#pragma pack(pop)
#endif

namespace ntlx
{
namespace cd
{

class NTLXSHARED_EXPORT Text
    : public Base<CDTEXT, _CDTEXT, SIG_CD_TEXT>
{
public:
  class NTLXSHARED_EXPORT FontId
  {
  public:
    FontId(FONTID id);
    FontId(const FontId& other);
    FontId& operator=(const FontId& other);

    BYTE size() const;
    BYTE color() const;
    bool isBold() const;
    bool isItalice() const;
    bool isUnderline() const;
    bool isStrikeOut() const;
    bool isSuper() const;
    bool isSub() const;
    bool isEffect() const;
    bool isShadow() const;
    bool isEmboss() const;
    bool isExtrude() const;
    BYTE face() const;

  private:
    FONTID id_;
  };

  Text(char** ppRecord);
  Text(const Text& other);
  Text& operator=(const Text& other);

  QString value() const;
  FontId fontId() const;

  virtual QString toString() const;

private:
  QString value_;
};

} // namespace cd

} // namespace ntlx

#endif // NTLX_CD_TEXT_H

APIで提供される、FONTIDFIELDSから必要な情報と取得するための仕組みは、ほぼマクロなので実装もあまり難しいことはないと思います。

// cd/text.cpp

#include "ntlx/cd/text.h"

#include "ntlx/lmbcs.h"

namespace ntlx
{
namespace cd
{

Text::Text(char** ppRecord)
  : Base<CDTEXT, _CDTEXT, SIG_CD_TEXT>(ppRecord)
  , value_()
{
  WORD len = record_.Header.Length - ODSLength(_CDTEXT);
  Lmbcs lmbcs(*ppRecord, len);
  value_ = lmbcs.toQString();
  nullToCR(value_);
}

Text::Text(const Text &other)
  : Base<CDTEXT, _CDTEXT, SIG_CD_TEXT>(other)
  , value_(other.value_)
{
}

Text& Text::operator=(const Text& other)
{
  Base<CDTEXT, _CDTEXT, SIG_CD_TEXT>::operator=(other);
  if (this != &other)
    value_ = other.value_;
  return *this;
}

QString Text::value() const
{
  return value_;
}

Text::FontId Text::fontId() const
{
  return Text::FontId(record_.FontID);
}

QString Text::toString() const
{
  return value();
}

Text::FontId::FontId(FONTID id)
  : id_(id)
{
}

Text::FontId::FontId(const Text::FontId &other)
  : id_(other.id_)
{
}

Text::FontId& Text::FontId::operator=(const Text::FontId& other)
{
  if (this == &other) return *this;
  id_ = other.id_;
  return *this;
}

BYTE Text::FontId::size() const
{
  return FontGetSize(id_);
}

BYTE Text::FontId::color() const
{
  return FontGetColor(id_);
}

bool Text::FontId::isBold() const
{
  return FontIsBold(id_);
}

bool Text::FontId::isItalice() const
{
  return FontIsItalic(id_);
}

bool Text::FontId::isUnderline() const
{
  return FontIsUnderline(id_);
}

bool Text::FontId::isStrikeOut() const
{
  return FontIsStrikeOut(id_);
}

bool Text::FontId::isSuper() const
{
  return FontIsSuperScript(id_);
}

bool Text::FontId::isSub() const
{
  return FontIsSubScript(id_);
}

bool Text::FontId::isEffect() const
{
  return FontIsEffect(id_);
}

bool Text::FontId::isShadow() const
{
  return FontIsShadow(id_);
}

bool Text::FontId::isEmboss() const
{
  return FontIsEmboss(id_);
}

bool Text::FontId::isExtrude() const
{
  return FontIsExtrude(id_);
}

BYTE Text::FontId::face() const
{
  return FontGetFaceID(id_);
}

} // namespace cd

} // namespace ntlx

Notesの古いバージョンでは、色数の制限がひどく、たいした表現ができなかったことを思い出します。その片鱗が、このようにAPI上には残っているんですね。

CDデータのテンプレート化 - リッチテキスト - 「NotesPeek」をQtでリメイク

前回は、リッチテキストの要素である「CDデータ」を、未定義のものでもバイナリの値を並べて表示できるCdOtherItemクラスと、ライブラリクラスとしてのntlx::cd::Otherクラス、そしてその親クラス(と言うより、インターフェース、純粋仮想関数しか持っていない)となるntlx::Cdクラスを作成しました。定義の手法に関しては、我が座右の書、Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングを参考にさせていただいています。改めて感謝です。

ここで「CDデータ」と呼んでいるものについて簡単におさらいしておきます。

  • Notesデータベースに含まれるもの → Note文書(ドキュメント、設計文書、ACLなど)
  • Note文書に含まれるもの → アイテム(文字列、数値、日時、リッチテキスト、画像、ファイル、設計要素(フィールド、列など))
  • リッチテキストアイテムに含まれるもの → CDデータ(テキスト、段落、画像、表、色、ホットスポットなど)

細かい点は大目に見ていただき、ここで伝えたいのは、CDデータはリッチテキストアイテムの構成要素であるということです。

リファレンスを見ると、「CD」とは「Composite Data」の略であることが分かるので、「データ」が重複しているのは否めませんが、「CD」という接頭辞が一つの大きなグループという位置づけにもなっており、特定の要素を表さない汎用的な名称として、あえて「CDデータ」と呼ばせていただいています。

さて、前回はCdクラスからcd::Otherクラスを派生させました。今回のゴールはCDTEXTというリッチテキストの中のテキスト要素を専用表示させるため、cd::Textクラスを派生させます。ただし、Cdクラスとは別にCDデータが共通して行う処理も多いので、テンプレートクラスcd::Baseを間に挟みます。

// ntlx/cd/base.h

#ifndef NTLX_CD_BASE_H
#define NTLX_CD_BASE_H

#include <ntlx/cd.h>

#if defined(NT)
#pragma pack(push, 1)
#endif

#include <ods.h>

#if defined(NT)
#pragma pack(pop)
#endif

namespace ntlx
{
namespace cd
{

template <typename CDxxx, WORD _CDxxx, WORD SIG_CD_xxx>
class NTLXSHARED_EXPORT Base
    : public Cd
{
public:
  Base(char** ppRecord)
  {
    ODSReadMemory(ppRecord, _CDxxx, &record_, 1);
  }

  Base(const Base& other)
    : record_(other.record_)
  {
  }

  Base& operator=(const Base& other)
  {
    if (this != &other)
      record_ = other.record_;
    return *this;
  }

  virtual WORD getSignature() const
  {
    return SIG_CD_xxx;
  }

  virtual WORD odsLength() const
  {
    return record_.Header.Length + record_.Header.Length % 2;
  }

protected:
  mutable CDxxx record_;
};

} // namespace cd

} // namespace ntlx

#endif // NTLX_CD_BASE_H

純粋仮想メソッドCd::toString以外は、テンプレートとした構造体CDxxxを使って表すことができます。CDxxxに当てはまる構造体は300以上もあるので、テンプレートクラスにするこの方法は、理にかなっていてとてもいいと思います。これはほぼそっくり座右の書のアイデアをお借りしています。

  • CDxxx: CDデータの先頭を表す構造体
  • _CDxxx: CDデータシンボル(ODSLength用)
  • SIG_CD_xxx: CDデータシグネチャ(CD構造体先頭用)

これを親として、cd::Textクラスを定義します。

// ntlx/cd/text.h

#ifndef NTLX_CD_TEXT_H
#define NTLX_CD_TEXT_H

#include <ntlx/cd/base.h>

#if defined(NT)
#pragma pack(push, 1)
#endif

#include <editods.h>

#if defined(NT)
#pragma pack(pop)
#endif

namespace ntlx
{
namespace cd
{

class NTLXSHARED_EXPORT Text
    : public Base<CDTEXT, _CDTEXT, SIG_CD_TEXT>
{
public:
  Text(char** ppRecord);
  Text(const Text& other);
  Text& operator=(const Text& other);

  QString value() const;

  virtual QString toString() const;

private:
  QString value_;
};

} // namespace cd

} // namespace ntlx

#endif // NTLX_CD_TEXT_H
// cd/text.cpp

#include "ntlx/cd/text.h"

#include "ntlx/lmbcs.h"

namespace ntlx
{
namespace cd
{

Text::Text(char** ppRecord)
  : Base<CDTEXT, _CDTEXT, SIG_CD_TEXT>(ppRecord)
  , value_()
{
  WORD len = record_.Header.Length - ODSLength(_CDTEXT);
  Lmbcs lmbcs(*ppRecord, len);
  value_ = lmbcs.toQString();
  nullToCR(value_);
}

Text::Text(const Text &other)
  : Base<CDTEXT, _CDTEXT, SIG_CD_TEXT>(other)
  , value_(other.value_)
{
}

Text& Text::operator=(const Text& other)
{
  Base<CDTEXT, _CDTEXT, SIG_CD_TEXT>::operator=(other);
  if (this != &other)
    value_ = other.value_;
  return *this;
}

QString Text::value() const
{
  return value_;
}

QString Text::toString() const
{
  return value();
}

} // namespace cd

} // namespace ntlx

cd::Text固有の実装としてはQString value_メンバ変数を持っています。テキストのCDデータは、CDTEXT構造体に続いてLmbcs形式のテキストが収められているので、BaseコンストラクタがCDTEXT構造体分のデータを取得したあとに、Lmbcs文字列をvalue_に、QStringに変換しながら格納しています。Baseコンストラクタが呼び出しているODSReadMemoryは、CDデータ構造体を読み込むと、メモリのポインタをそのサイズ分だけ移動してくれるので便利です。

これをNSFinderに実装してみた例は以下の通りです。

f:id:takahide-kondoh:20170820215659p:plain

前回までの、汎用的なCDデータの出力ではなくて、CDTEXT専用の出力として機能しています。

未知のCDデータの表示 - リッチテキスト - 「NotesPeek」をQtでリメイク

ちょっと前の記事「伝説のツール「NotesPeek」をQtでリメイク開発記2017-7-23 - Chiburu Systemsのブログ」で、リッチテキストについて着手したことをご報告しました。この中で、リッチテキストは多種多様な「CDxxxx」という複合データ(Composite Date)によって構成されているとお伝えしました。

ここで簡単にまとめてみます。

  • リッチテキストはCDデータの連続体である。
  • CDデータは9.0.1において150種類以上ある。
  • 1つのCDデータはヘッダー情報とコンテンツ情報でできている。
  • ヘッダー情報はサイズによってBSIG、WSIG、LSIGの3種類ある。

座右の書Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングでは、紙面の都合で代表的なCDデータのクラス実装例を紹介するにとどめていますが、一方でどんなCDデータでも取り込んだり書き込んだりできる「CdOtherクラス」というのを紹介していました。ポイントは、CDデータのポインタが渡されると、ヘッダー情報を正しく分析して、BSIG、WSIG、LSIGのいずれかであることを判別して、コンテンツ情報を切り分けるようになっていることです。ヘッダー情報とコンテンツ情報を正しく分離できれば、あとの作業は共通化することができます。

本プロジェクトのNSFinderでは、いったんはCDTEXTのみを判別してテキスト化し、その他のCDデータはいったん無視する方針で実装しましたが、やはりきちんと実装していきたいので、まずはCdOtherのような汎用CDデータクラスで処理できるようにします。

// ntlx/cd.h

#ifndef NTLX_CD_H
#define NTLX_CD_H

#include <ntlx_global.h>
#include <ntlx/status.h>

namespace ntlx
{

class NTLXSHARED_EXPORT Cd
{
public:
  virtual WORD getSignature() const = 0;
  virtual WORD odsLength() const = 0;
  virtual QString toString() const = 0;
};

} // namespace ntlx

#endif // NTLX_CD_H

まず、元となるクラス「ntlx::Cd」です。シグネチャの取得、データ長の取得、そして文字列に変換するメソッドをすべて純粋仮想関数で定義しているインターフェースクラスです。

これを継承した汎用クラスが「ntlx::cd::Other」です。

// ntlx/cd/other.h

#ifndef NTLX_CD_OTHER_H
#define NTLX_CD_OTHER_H

#include <ntlx/cd.h>

namespace ntlx
{
namespace cd
{

class NTLXSHARED_EXPORT Other
    : public Cd
{
public:
  Other(char** ppRecord);
  Other(const Other& other);
  Other& operator=(const Other& other);

  virtual WORD getSignature() const;
  virtual WORD odsLength() const;
  virtual QString toString() const;

protected:
  QByteArray record_;
};

} // namespace cd

} // namespace ntlx

#endif // NTLX_CD_OTHER_H

本家Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングでは、CdOtherとして紹介されていましたが、ここでは2層の名前空間の中で定義しています。また、データ本体の扱い方を、char文字配列からQByteArrayに変更しました。QByteArrayの方が扱いがはるかに楽であるためです。

// cd/other.cpp

#include "ntlx/cd/other.h"
#include <QStringList>

#if defined(NT)
#pragma pack(push, 1)
#endif

#include <ods.h>

#if defined(NT)
#pragma pack(pop)
#endif

namespace ntlx
{
namespace cd
{

Other::Other(char** ppRecord)
  : Cd()
  , record_()
{
  int len = 0;
  switch (((uchar*)*ppRecord)[1])
  {
  case 0x00: // Long Signature
    len = ((LSIG*)*ppRecord)->Length;
    break;

  case 0xff: // Word Signature
    len = ((WSIG*)*ppRecord)->Length;
    break;

  default: // Byte Signature
    len = ((BSIG*)*ppRecord)->Length;
    break;
  }

  record_ = QByteArray(*ppRecord, len);
}

Other::Other(const Other& other)
  : Cd()
  , record_(other.record_)
{
}

Other& Other::operator=(const Other& other)
{
  if (this != &other)
    record_ = other.record_;
  return *this;
}

WORD Other::getSignature() const
{
  switch (((uchar*)record_.constData())[1])
  {
  case 0x00: return ((LSIG*)record_.constData())->Signature;
  case 0xff: return ((WSIG*)record_.constData())->Signature;
  }
  return ((BSIG*)record_.constData())->Signature;
}

WORD Other::odsLength() const
{
  return (WORD)(record_.length() + record_.length() % 2);
}

QString Other::toString() const
{
  QStringList list;
  for (auto it = record_.constBegin(); it != record_.constEnd(); ++it)
  {
    uchar b = (uchar)(*it);
    list.append(QString("%1").arg((uint)b, 4, 16, QChar('0')).right(2));
  }
  return list.join(" ");
}

} // namespace cd

} // namespace ntlx

ほとんど本家Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングを参考にしています。

odsLengthメソッドは実データサイズではなくODSとしてメモリ上にどれくらいの長さを持っているかということで、これが必ず偶数値を取ることから、「~% 2」の値が加算されています。

toStringメソッドはQtのライブラリを使って、バイトデータを16進数文字列に変換しています。

このOtherクラスを用いてNSFinderを実装した例(Windows)をご紹介しておきます。

f:id:takahide-kondoh:20170813221500p:plain

伝説のツール「NotesPeek」をQtでリメイク開発記2017-8-6

前回は、設計文書のうち、フォーム文書を展開してみました。今回はその続きです。

前回も触れましたが、Notesの設計文書は基本的に「文書クラス」というものでその性質が分けられますが、バージョンが進むうちに文書クラスの範囲(WORDビットの下位12ビット)では到底カバーできず、「フラグ」というテキストを併用することで正しく設計文書を認識することができます。

設計文書のフラグは、「$Flags」という単なるテキストフィールドです。

// stdnames.h

/* Design flags */
#define DESIGN_FLAGS "$Flags"

このフィールドに、1文字1文字に意味を持たせた文字フラグが列記されています。代表的なフラグを抜粋してみます。

// stdnames.h

// for ALL
#define DESIGN_FLAG_DEFAULT_DESIGN 'd' /* ALL: Indicates the default design note for it's class (used for VIEW) */

// for FORM
#define DESIGN_FLAG_FILE 'g' /* FORM: file design element */
#define DESIGN_FLAG_IMAGE_RESOURCE 'i' /* FORM: Note is a shared image resource */
#define DESIGN_FLAG_XSPPAGE 'K' /* FORM: with "g", design element is an xpage, much like a file resource, but special! */
#define DESIGN_FLAG_SUBFORM 'U' /* FORM: Indicates that a form is a subform.*/
#define DESIGN_FLAG_WEBPAGE 'W' /* FORM: Note is a WEBPAGE */
#define DESIGN_FLAG_FRAMESET '#' /* FORM: Indicates that this is a frameset note */ 

// for VIEW
#define DESIGN_FLAG_CALENDAR_VIEW 'c' /* VIEW: Indicates a form is a calendar style view. */
#define DESIGN_FLAG_FOLDER_VIEW 'F' /* VIEW: This is a V4 folder view. */
#define DESIGN_FLAG_SHARED_COL '^' /* VIEW: shared column design element */

// for FILTER(AGENT)
#define DESIGN_FLAG_LOTUSSCRIPT_AGENT 'L' /* FILTER: If its LOTUSSCRIPT */
#define DESIGN_FLAG_SCRIPTLIB 's' /* FILTER: A database global script library note */
#define DESIGN_FLAG_JAVA_AGENT 'J' /* FILTER: If its Java */
#define DESIGN_FLAG_DATABASESCRIPT 't' /* FILTER: A database script note */
#define DESIGN_FLAG_SERVLET 'z' /* FILTER: this is a servlet, not an agent! */

// Others
#define DESIGN_FLAG_JAVASCRIPT_LIBRARY 'h' /* Javascript library. */
#define DESIGN_FLAG_CONNECTION_RESOURCE 'k' /* Data Connection Resource (DCR) for 3rd party database */
#define DESIGN_FLAG_JSP '<' /* this design element is a jsp */
#define DESIGN_FLAG_SACTIONS 'y' /* Shared actions note */
#define DESIGN_FLAG_STYLESHEET_RESOURCE '=' /* Style Sheet Resource (SSR) */
#define DESIGN_FLAG_XSP_CC ';' /* note class form, a custom control */
#define DESIGN_FLAG_JAVAFILE '[' /* Java design element */

サブフォーム、フレームセット、ファイルリソース、イメージリソース、XPagesなどはフォームクラスから分岐しています。 フォルダ、カレンダービュー、共有列はビュークラスから分岐しています。 LotusScriptエージェント、Javaエージェント、データベーススクリプトなどはフィルタークラスから分岐しています。サーブレットも含まれていますが、どういうことでしょう?JSPも見受けられますね。

これらはフラグなので、文字列単位で比較するのではなく、所定の1文字が含まれていれば「フラグが立っている」ことになります。例えば、メールDBの「(GroupCalendarDlg)」というサブフォームには、以下のフラグが混在しています。

DESIGN_FLAG_ADD               // A
DESIGN_FLAG_NO_COMPOSE        // C
DESIGN_FLAG_SUBFORM           // U
DESIGN_FLAG_SUBFORM_NORENDER  // x
DESIGN_FLAG_HIDE_FROM_V3      // 3

$Flagsフィールドには、「CUA3x」という文字列で存在しています。1文字1文字の意味を抽出することで、この設計文書にはどんな属性が含まれているのかがわかります。

リメイクアプリ「nsfinder」では、今回これを確認できるようにしてみました。

f:id:takahide-kondoh:20170806161352p:plain

$Flagsフィールドだけでは、意味がわかりづらいので、「Design flags」というプロパティを設け、1つ1つのフラグ文字列に紐付く意味がわかるようにしました。

NotesPeekでは、これをアイコンのレベルでわかるようにしています。Notesの設計文書を、NotesPeekのまねをすることなく、アイコンレベルで表記できるようにするのは至難の業かもしれません。まずは文字ベースで表記できるようにし、実装に余裕が出てきたらアイコンも検討してみましょう。

伝説のツール「NotesPeek」をQtでリメイク開発記2017-7-30

前回はリッチテキストに対して切り込みましたが、今回は設計要素(設計文書)に対して切り込みを入れます。

Notesの設計要素は、広義では「文書(Note)」に当たります。もし、データとしての「文書」と分けるのであれば、「Document」がNotesで一般的に使われる文書に当たります。設計要素もデータ文書も、保存形式としては「Note」という単位で保存されるんですね。これが「Notes」という名前の所以なのかもしれません。

では、API的にはどのように設計要素を区別するのでしょうか。

文書(Note)には「文書クラス」というパラメータがあります。一般的なものを列挙してみます。

シグネチャ 意味
NOTE_CLASS_DOCUMENT 0x0001 データ文書/ドキュメント
NOTE_CLASS_INFO 0x0002 データベースについて
NOTE_CLASS_FORM 0x0004 フォーム
NOTE_CLASS_VIEW 0x0008 ビュー
NOTE_CLASS_ICON 0x0010 アイコン
NOTE_CLASS_DESIGN 0x0020 設計文書を含む内部ビュー
NOTE_CLASS_ACL 0x0040 アクセス制御リスト
NOTE_CLASS_HELP_INDEX 0x0080 Notesクライアントのヘルプのインデックス
NOTE_CLASS_HELP 0x0100 データベースの使い方
NOTE_CLASS_FILTER 0x0200 エージェント
NOTE_CLASS_FIELD 0x0400 共有フィールド
NOTE_CLASS_REPLFORMULA 0x0800 複製式

「設計文書を含む内部ビュー」?「ヘルプのインデックス」?さらりと書きましたが、今ひとつピンときません。愛読書Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングでも、このあたりを特別掘り下げているわけではないようです。今回は遺憾ながらその点についてはスルーします。

さて、設計要素のフォームやビューは、確かに文書クラスによってドキュメントとは区別されているのは分かりました。しかし、設計要素はこれだけだったでしょうか。サブフォームは?フレームセットは?フォルダは?共有アクションは?XPagesは?

安心して下さい。あります。

ただ、このようないわゆる「後発の」設計要素は文書クラスではなく、フラグ($Flagsアイテム)によって区別されます。フラグは単なる文字列で、サブフォームは'U'という文字で識別します。シグネチャとしては「DESIGN_FLAG_SUBFORM」として定義されています。後付けのゴチャゴチャ感は否めませんが、とにもかくも、文書クラスでざっくり分けたあとに、フラグで詳しく設計文書を識別する必要があります。サブフォームの場合は「フォームクラス」の「サブフォームフラグを含む」設計文書を見つければいいということになります。

それでは、今回の開発進捗の報告です。

今回は、設計要素、特にフォームを中心に設計要素を見られるようにしてみました。

f:id:takahide-kondoh:20170730171832p:plain

「Form Elements」というカテゴリを展開すると、文書クラスがフォームである設計文書を見ることができます。この画像ではメールデータベースを見ていますが、GIF画像だらけですね。GIF画像も文書クラスではフォームに分類されるということになります。その下にはXML文書も確認できますが、これは「ファイル」というくくりの設計文書になっていると思います。

f:id:takahide-kondoh:20170730172255p:plain

メールフォームのアイテムを展開して見てみます。これはドキュメントではなくフォームの設計文書なので、「$SubForm_RepIDs」のような見慣れないアイテムも存在します。これはきっとサブフォーム用の何かでしょう。

f:id:takahide-kondoh:20170730172612p:plain

さらに、「$Body」というコンポジットアイテム、いわゆるリッチテキストを見てみます。これは、フォームの中身の設計を表していて、ドキュメントのリッチテキストではお目にかかれない、フィールド用のデータ(EXT2_FIELD)も見ることができています。