伝説のツール「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)も見ることができています。

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

さて、ようやくリッチテキストにメスを入れる時が来ました。The time has come!

リッチテキストは、まぎれもなくアイテム(フィールド)なので、テキストフィールドや数値フィールドと同じように、アイテム型(WORD)を先頭に持つバイナリデータです。

アイテム型
TYPE_TEXT テキストフィールドデータ
TYPE_NUMBER 数値フィールドデータ
TYPE_TIME 日時フィールドデータ
TYPE_COMPOSITE リッチテキストフィールドデータ

なので、肝心なのは値の並べ方がどのようになっているかということになります。

リッチテキストは、書式付きの文字列や図、添付ファイルのようなデータが混在します。そのため、その複合データを混在させるために、データの種類と長さを表す「シグネチャ」と各データ固有の情報とを一組としたデータの羅列で構成されます。

例えば、リッチテキストに、何の変哲もない文字列を1行だけ入れたとします。これをNSFinderで覗いています。

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

NSFinderで「Body」フィールドを見てみると、以下のように「(CD)PABDEFINITION」、「(CD)PABREFERENCE」、「(CD)TEXT」の3つで構成されているのがわかります。

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

CDPABDEFINITIONは、段落の定義になります。

typedef struct {
   WSIG Header;        /* Used to quickly recognize structure type */
   WORD PABID;         /* ID of this PAB */
   WORD JustifyMode;   /* paragraph justification type */
   WORD LineSpacing;   /* (2*(Line Spacing-1)) (0:1,1:1.5,2:2,etc) */
   WORD ParagraphSpacingBefore; /* # LineSpacing units above para */
   WORD ParagraphSpacingAfter;  /* # LineSpacing units below para */
   WORD LeftMargin;    /* leftmost margin, twips rel to abs left */
                       /* (16 bits = about 44") */
   WORD RightMargin;   /* rightmost margin, twips rel to abs right */
                       /* (16 bits = about 44") */
                       /* Special value "0" means right margin */
                       /* will be placed 1" from right edge of */
                       /* paper, regardless of paper size. */
   WORD FirstLineLeftMargin; /* leftmost margin on first line */
                       /* (16 bits = about 44") */
   WORD Tabs;          /* number of tab stops in table */
   SWORD Tab[MAXTABS]; /* table of tab stop positions, negative */
                       /* value means decimal tab */
                       /* (15 bits = about 22") */
   WORD Flags;         /* paragraph attribute flags - PABFLAG_xxx */
   DWORD TabTypes;     /* 2 bits per tab */
   WORD Flags2;        /* extra paragraph attribute flags - PABFLAG2_xxx */
} CDPABDEFINITION;

先頭にある「WSIG Header」がシグネチャです。PABIDは段落定義のIDで、PABREFERENCEから参照されます。JustifyModeは行揃えの定義です。その他にマージンやタブの定義も見て取れます。

CDPABREFERENCEは、段落定義を参照します。

typedef struct {
   BSIG Header;
   WORD PABID; /* ID number of the CDPABDEFINITION */
               /* used by this paragraph */
} CDPABREFERENCE;

参照だけなので、シグネチャ部分を除けばPABIDのみのシンプルな構造です。

CDTEXTは、文字列データです。

typedef struct {
   WSIG   Header; /* Tag and length */
   FONTID FontID; /* Font ID */
/* The 8-bit text string follows... */
} CDTEXT;

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

FONTID自体は単なるDWORD型ですが、実はFONTIDFIELDSという構造体でもあります。ご覧の通り、フォントの書式設定が含まれています。

CDTEXTのデータだけでは、文字列本体は取得できません。CDTEXTに続いて文字列データがLMBCS形式で存在します。Headerデータには値の全体の長さが含まれるので、その全体の長さからCDTEXT構造体のサイズ分引けば、残りが文字列の長さとなります。

各CD〜の先頭には「BSIG Header」や「WSIG Header」がありましたが、そのほかに「LSIG Header」が存在します。これらはデータの長さに応じて「Byte長シグネチャ」「Word長シグネチャ」「Long長シグネチャ」を表しています。前述のCDPABREFERENCEのような参照IDのみのような短いもの、テキストのような一般的なもの、画像のような巨大なもの、それぞれに合わせて使い分けられています。

Notesのアイテム型もそこそこ種類がありますが、リッチテキストの中身を構成するコンポジットデータの種類はその数をしのぐでしょう。ただし、このリッチテキストアイテムを制することができれば、他の書式(HTMLやWord、その他リッチテキスト書式など)との相互変換をする場合に必要になるので、対応できるコンポジットデータの数を増やしておくことは、非常に大きな武器になることは間違いないでしょう。

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

リッチテキストの分解をする前に、どうしてもやっておきたいことがありました。今回はそちらの開発について。

前回、文書のアイテム(フィールド)を子アイテムにし、すべてのアイテムのデータについて、一律でバイナリ形式の文字列に変更して、表記することにしました。今回は、テキスト型、数値型、日時型とそれぞれのリスト型を、それぞれの表現に見合った形式で表記し直すことにしました。これらのデータ型は基本中の基本なので、リッチテキストに着手する前にどうしてもやっておきたかったのです。

最初は単一テキスト型です。

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

次に単一数値型です。

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

次に単一日時型です。

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

次はテキストリスト型です。

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

次は数値リスト型です。

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

最後に日時リスト型です。

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

数値リストと日時リストは波括弧({})2つでくくられており、1つめがシングルリスト、2つめがペアリスト(範囲型のリスト)になっています。今回のサンプルには出てきていませんが、日時リスト型が範囲のみで定義されていれば、以下のような出力になるはずです。

{}{2017/07/17 11:59:58 - 2017/07/17 23:59:58,2017/07/17 11:59:58 - 2017/07/17 23:59:58}

これで懸念事項は完了したので、次回はいよいよリッチテキストにメスを入れます。

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

先週から主旨を「開発記」に変更しての2回目です。

今週からいよいよ一階層下に進めます。文書に含まれるフィールド(アイテム)について展開していきます。まずは、NotesPeekのフィールドを展開した様子を見てみます。

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

文書を展開すると、デフォルトの「Note Information(文書情報)」があり、以降は文書内に含まれるフィールドのリストが展開されます。フィールドの種類によって、1つ1つにタイプを表すアイコンが割り当てられているのが印象的です。よく見ると、読者や作成者のフィールドにも個別にアイコンが割り当てられています。「読者」や「作成者」は、正確には「Type(型)」ではなく「フラグ」であり、フラグ付きのテキスト、またはテキストリスト型になります。

今回はここまでをゴールとしたかったんですが、諸事情によりアイコンは一律、値の表記方法もバイナリ形式で一律となっています。次の画面は、先ほどのNotesPeekと同じ文書を展開した時の画面サンプルです。

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

微妙に表記方法が違ったり、フィールドの並び順がABC順になっていなかったりしますが、おおよそフィールドデータをリストアップする機能についてはざっくり実装できました。

次回以降の目標としては、データ型によって値の表記方法を変えてデータを見やすくしたり、フィールドの並び順を統一するなどがあるでしょう。また、さらに深く進めていき、リッチテキストの内部データをリストアップすることも、近いうちに実装していこうと思います。リッチテキストが解析できると、他の形式とコンバートに使えるので、技術的にもいろいろと武器になると思います。ではまた来週。

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

NotesPeekというNotesのデータベースを文書、フィールド、リッチテキスト要素のレベルまで細分化して解析できるツールに、最新のAPIバージョンと、マルチプラットフォームC++ライブラリでリメイクに挑んでいるこの企画。

一銭にもなりませんが、どうしても作ってみたくて、個人的な趣味で作っているこの企画。

今までNotesのAPIについての仕組みや洞察、C++ライブラリ化の仕方などを、愛読書Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングからの引用と私なりの新しい解釈でアレンジして紹介してきましたが、本業が多忙になったことも相まって、肝心のリメイクがはかどりません。

そこで、今週からAPIの紹介などをいったん後回しにして、週に一度は何らかの進捗をして、その様子をお伝えしていこうと思います。

今週は、NotesPeekでデータベースが展開する6つのサブカテゴリを追加したので、その紹介です。

NotesPeekでデータベース(画面ではログデータベース)を見ると、原則として6つのサブフォルダがあるのがわかります。

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

  1. Database Information (データベース情報)
  2. Access Control List (アクセス制御リスト)
  3. Design Elements (設計要素)
  4. Documents (文書)
  5. Profiles (プロフィール文書)
  6. Deletion Stubs (削除スタブ)

ここで「Recently Peeked Notes」は、「最近覗いた文書」ということで、この機能は後回しにしようと思います。

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

「Documents」にはすでに見つかっている文書の数も出ているので、情報としてはすでに取得済みと考えられます。

現在開発中のNotesPeekリメイクアプリ「NSFinder」で同じログデータベースを見ると、以下のようになります。

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

もう少し下の方になります。NotesPeekはディレクトリ、Notesデータベース、Notesテンプレートの順に取得してきているようですが、NSFinderでは一括で取ってきています。いずれ改善すべきポイントです。

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

ログデータベースが出てきました。前回から各アイテムにはアイコンも追加してあります。

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

文書を展開するロジックは、すでにNoteFileItemというクラスで実装済みでしたが、その機能をこの「Documents」と表示するアイテムクラスに移植しました。なので、細かなUIの調整が主で、APIの面ではたいしたことはしていません。

今後の方針ですが、少し悩んでいます。文書を深掘りして、フィールド、リッチテキスト要素と「下へ、下へ」掘っていくべきか、ACLや設計要素など、「横へ、横へ」広げていくべきか。次回までには決めておかないと、先に進めなくなってしまうので、今日、明日には結論を出そうと思っています。ではまた来週。

伝説のツール「NotesPeek」をQtでリメイクする(その13・名前)

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

Notesが扱うデータの基本型は、テキスト、数値、日時の3つとその複数値です。それ以外については、おおよそ次の3つに分けられます。1つはリッチテキスト、2つめがバイナリ形式、そして3つめが基本型に応用になります。2つめのバイナリ形式は、画像や添付ファイル、@式などがそれにあたります。そして3つめの「基本型の応用」とは、基本型に書式や属性によって意味を持たせたものを指します。

例えば今回のお題「名前」は、「型」という意味では単なる「テキスト」です。しかし、「CN=Taro Yamada/OU=Sales/O=Acme」という「書式」を持つことで「名前型」というデータになります。

Notes APIにおいて、「名前型」はDNという接頭辞で始まる関数を使います。Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングによると、「Domain Name」(領域名)の略だとあります。一方、APIのリファレンスには「Distinguished Name」(識別名)のように書かれています。訳語からいっても後者ではないかと思います。ただ、残念なことに今回の実装には「Domain Name」の方でクラスを構築してしまったので、次のバージョンでは修正したいと思っています(少々綴りが長いですが)。

識別名クラスには、1つのメンバ変数(Lmbcs型)を保持するシンプルクラスです。ルールとして、このメンバ変数に保存する識別名は常に「基準書式(Canonical)」であることとします。

最初にヘッダーファイルの実装例をご覧ください。

// ntlx/domainname.h

#ifndef NTLX_DOMAINNAME_H
#define NTLX_DOMAINNAME_H

#include <ntlx/lmbcs.h>

namespace ntlx
{

/**
 * @brief ドメイン名(基準書式名)クラス
 */
class NTLXSHARED_EXPORT DomainName
    : public IStatus
{
public:
  /**
   * @brief デフォルトコンストラクタ
   */
  DomainName();

  /**
   * @brief コンストラクタ(LMBCS文字列から)
   * @param name 書式化する名前文字列(LMBCS)
   */
  DomainName(const Lmbcs& name);

  /**
   * @brief コンストラクタ(QString文字列から)
   * @param name 書式化する名前文字列(QString)
   */
  DomainName(const QString& name);

  /**
   * @brief コピーコンストラクタ
   * @param other コピー元
   */
  DomainName(const DomainName& other);

  /**
   * @brief 代入演算子
   * @param other 代入元
   * @return 自身への参照
   */
  DomainName& operator=(const DomainName& other);

  /**
   * @brief 文字列が空なら真
   * @return 真/偽
   */
  bool isEmpty() const { return name_.isEmpty(); }

  /**
   * @brief 基準書式をLMBCSで返す
   * @return 基準書式LMBCS
   */
  Lmbcs canonical() const { return name_; }

  /**
   * @brief 省略書式をLMBCSで返す
   * @return 省略書式LMBCS
   */
  Lmbcs abbreviated() const;

  /**
   * @brief 代入演算子
   * @param name 代入元LMBCS
   * @return
   */
  DomainName& operator=(const Lmbcs& name);

  /**
   * @brief 代入演算子
   * @param name 代入元QString
   * @return
   */
  DomainName& operator=(const QString& name);

  /**
   * @brief 等価演算子
   * @param lhs 左辺
   * @param rhs 右辺
   * @return 等価なら真
   */
  friend bool operator==(const DomainName& lhs, const DomainName& rhs);

  /**
   * @brief 不等価演算子
   * @param lhs 左辺
   * @param rhs 右辺
   * @return 不透過なら真
   */
  friend bool operator!=(const DomainName& lhs, const DomainName& rhs);

protected:
  void canonicalize(const Lmbcs& name);

private:
  Lmbcs name_;
};

} // namespace ntlx

#endif // NTLX_DOMAINNAME_H

次に、ソースファイルの実装例です。

// domainname.cpp

#include "ntlx/domainname.h"

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

#include <dname.h>
#include <names.h>

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

namespace ntlx
{

DomainName::DomainName()
  : IStatus()
  , name_()
{
}

DomainName::DomainName(const Lmbcs& name)
  : IStatus()
  , name_()
{
  canonicalize(name);
}

DomainName::DomainName(const QString& name)
  : IStatus()
  , name_()
{
  canonicalize(Lmbcs::fromQString(name));
}

DomainName::DomainName(const DomainName &other)
  : IStatus()
  , name_(other.name_)
{
}

DomainName& DomainName::operator=(const DomainName& other)
{
  if (this == &other) return *this;
  name_ = other.name_;
  return *this;
}

Lmbcs DomainName::abbreviated() const
{
  char abbreviate[MAXUSERNAME];
  WORD len;
  lastStatus_ = DNAbbreviate(
        0L
        , nullptr
        , name_.constData()
        , abbreviate
        , MAXUSERNAME
        , &len
        );
  if (lastStatus().success())
    return Lmbcs(abbreviate, len);
  return Lmbcs();
}

void DomainName::canonicalize(const Lmbcs& name)
{
  char buffer[MAXUSERNAME];
  WORD len;
  lastStatus_ = DNCanonicalize(
        0L
        , nullptr
        , name.constData()
        , buffer
        , MAXUSERNAME
        , &len
        );
  if (lastStatus().success())
    name_ = Lmbcs(buffer, len);
  else
    name_ = Lmbcs();
}

DomainName& DomainName::operator=(const Lmbcs& name)
{
  canonicalize(name);
  return *this;
}

DomainName& DomainName::operator=(const QString& name)
{
  canonicalize(Lmbcs::fromQString(name));
  return *this;
}

bool operator==(const DomainName& lhs, const DomainName& rhs)
{
  return lhs.name_ == rhs.name_;
}

bool operator!=(const DomainName& lhs, const DomainName& rhs)
{
  return !operator==(lhs, rhs);
}

} // namespace ntlx

ポイントは、abbreviatdメソッドとcanonicalizeメソッドでしょう。

まず、canonicalizeメソッドはLMBCS文字列を受け取って、それを基準書式に変換して内部のメンバ変数に保持します。

次にabbreviatedメソッドは、内部のメンバ変数を省略書式にしてLmbcsとして返します。

他にも識別名には関数が存在しますが、今回はここまでとします。

APIから見るEvernoteのノートとNotes

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

これは、Evernoteのノート、NoteのAPIから見た構造です。ぼーっと眺めてみます。

とてもNotesっぽいですね。例えばこの構造を、Notesでなじみのある用語と比べてみると・・・

guid = unid
title = subject/title field
content = body rich text field
created = created
updated = last modified
notebookGuid = replica id
resources = $FILE(添付ファイル)
tagNames (like) categories field

・・・といった感じでしょうか。

何ってわけではないんですが、Notesの文書にとても似てます。

相互に交換できるといいですよねぇ。

いえいえ、何ってわけではないんです。本当に。