伝説のツール「NotesPeek」をQtでリメイク開発記2017-7-2
NotesPeekというNotesのデータベースを文書、フィールド、リッチテキスト要素のレベルまで細分化して解析できるツールに、最新のAPIバージョンと、マルチプラットフォームC++ライブラリでリメイクに挑んでいるこの企画。
一銭にもなりませんが、どうしても作ってみたくて、個人的な趣味で作っているこの企画。
今までNotesのAPIについての仕組みや洞察、C++ライブラリ化の仕方などを、愛読書Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングからの引用と私なりの新しい解釈でアレンジして紹介してきましたが、本業が多忙になったことも相まって、肝心のリメイクがはかどりません。
そこで、今週からAPIの紹介などをいったん後回しにして、週に一度は何らかの進捗をして、その様子をお伝えしていこうと思います。
今週は、NotesPeekでデータベースが展開する6つのサブカテゴリを追加したので、その紹介です。
NotesPeekでデータベース(画面ではログデータベース)を見ると、原則として6つのサブフォルダがあるのがわかります。
- Database Information (データベース情報)
- Access Control List (アクセス制御リスト)
- Design Elements (設計要素)
- Documents (文書)
- Profiles (プロフィール文書)
- Deletion Stubs (削除スタブ)
ここで「Recently Peeked Notes」は、「最近覗いた文書」ということで、この機能は後回しにしようと思います。
「Documents」にはすでに見つかっている文書の数も出ているので、情報としてはすでに取得済みと考えられます。
現在開発中のNotesPeekリメイクアプリ「NSFinder」で同じログデータベースを見ると、以下のようになります。
もう少し下の方になります。NotesPeekはディレクトリ、Notesデータベース、Notesテンプレートの順に取得してきているようですが、NSFinderでは一括で取ってきています。いずれ改善すべきポイントです。
ログデータベースが出てきました。前回から各アイテムにはアイコンも追加してあります。
文書を展開するロジックは、すでにNoteFileItemというクラスで実装済みでしたが、その機能をこの「Documents」と表示するアイテムクラスに移植しました。なので、細かなUIの調整が主で、APIの面ではたいしたことはしていません。
今後の方針ですが、少し悩んでいます。文書を深掘りして、フィールド、リッチテキスト要素と「下へ、下へ」掘っていくべきか、ACLや設計要素など、「横へ、横へ」広げていくべきか。次回までには決めておかないと、先に進めなくなってしまうので、今日、明日には結論を出そうと思っています。ではまた来週。
伝説のツール「NotesPeek」をQtでリメイクする(その13・名前)
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
これは、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の文書にとても似てます。
相互に交換できるといいですよねぇ。
いえいえ、何ってわけではないんです。本当に。
伝説のツール「NotesPeek」をQtでリメイクする(その12・サーバリストの取得)
今回のNSFinderでは、NotesPeekと同様に、ツリービューのルートに「Local」「Servers」という2つのアイテムを配置して、LocalにはNotesクライアントのデータディレクトリを、Serversには利用可能なDominoサーバのリストを取得してアイテム化します。
サーバリストとは、NotesクライアントおよびAPIから見て、現在利用可能なDominoサーバのことで、サーバリストを取得するには、NSGetServerList関数を使用します。
// #include <ns.h> STATUS LNPUBLIC NSGetServerList (char far *pPortName, DHANDLE far *retServerTextList);
pPortNameは、ポート名をLMBCS形式で指定します。デフォルトでよければ0を渡します。retServerTextListは、サーバ名のテキストリストに対応したハンドルを取得するためのポインタになります。
返ってくるハンドルをロックすると、次の順番で格納されたメモリブロックを取得できます。
サーバの数count(サイズ:WORD)
サーバ名の長さ×サーバ数(サイズ:WORD×count)
サーバ名×サーバ数
これと似た構造に、「その11」で説明したテキストリストがあります。
伝説のツール「NotesPeek」をQtでリメイクする(その10・リスト型/範囲型) - Chiburu Systemsのブログ
先頭のWORD型がLIST構造体である以外は同じ構造です。また、LIST構造体はUSHORT型メンバーを1つしか持っておらず、そのUSHORT型は、32bitアプリではWORD型と同じなので、バイト数的にも同じになります。
それでは、私がこの関数を使って、NTLXライブラリにインプリメントした例をご紹介しておきます。まずはヘッダ部です。
// ntlx/status.h /** * @brief サーバリストを取得する * @param list 格納先となるQStringのリストオブジェクトへの参照 * @param port 検索するポート名 * @return 処理ステータス */ NTLXSHARED_EXPORT Status getServers( QList<QString>& list , const QString& port = "" );
ポート名はQString型、受け取るサーバリストはQList
// status.cpp Status getServers(QList<QString>& list, const QString& port) { HANDLE handle = NULLHANDLE; Status result = NSGetServerList( port.isEmpty() ? 0 : Lmbcs::fromQString(port).data() , &handle ); if (result.failure()) return result; Q_ASSERT(handle); WORD* pLen = (WORD*)OSLockObject(handle); WORD count = *pLen++; char* pValue = (char*)(pLen + count); for (WORD i = 0; i < count; ++i) { Lmbcs value(pValue, *pLen); list.append(value.toQString()); pValue += *pLen++; } OSUnlockObject(handle); OSMemFree(handle); return result; }
構造自体はテキストリストの時と大差はありません。ポイントは、受け取ったテキストリストのハンドルは、受け取った側の責任で解放する必要がありますので、OSMemFreeを使います。
サーバリストは、あくまで現在利用可能なものなので、この関数で取得できたDominoサーバしかないというわけではありません。実際にはドミノディレクトリなどの登録情報を正として、この関数は補助や疎通用として利用する方がよいでしょう。詳しい疎通情報には、NSPingServerという関数を使うことができます。この関数では通りやすさはを0〜100で返してきます。100がさくさく通っており、0が通らない状態です。
次回はライブラリ側の最後のトピックとして、「名前(DomainName)」を説明したいと思います。
伝説のツール「NotesPeek」をQtでリメイクする(その11・範囲型の読み込み)
前回は、Notes APIにおける複数値、LIST型とRANGE型について説明しました。テキストの複数値はLIST型を、数値と日時はRANGE型を使って複数値を表現しています。RANGE型は2種類のリストを持っており、1つは単数値のリスト、もう1つは「範囲」というペア値のリストを同時に持ちます。そしてこのペア値は、数値も日時もLowerとUpperという値を持っています。
数値の複数値も、日時の複数値も、値が数値か日時かの違いのみで、それ以外の構造はまったく同じです。ということは、C++テンプレートで実装するにはうってつけでしょう。
それでは、どのように実装してみたのか、ソースコードを見ながら説明していきます。
今回、複数値も含めてNSFSearch関数で取得したサマリーバッファを格納するクラスとして、ntlx::SummaryMapクラスを定義しました。
// ntlx/summarymap.h抜粋 namespace ntlx { /** * @brief 名前付きサマリーバッファ用データクラス */ class NTLXSHARED_EXPORT SummaryMap { public: /** * @brief デフォルトコンストラクタ */ SummaryMap(); /** * @brief コピーコンストラクタ * @param other コピー元 */ SummaryMap(const SummaryMap& other); /** * @brief 代入演算子 * @param other 代入元 * @return 自身への参照 */ SummaryMap& operator=(const SummaryMap& other); /** * @brief 検索マッチ情報 * @return SEARCH_MATCH型データへの参照 */ const SEARCH_MATCH& match() const { return match_; } QMap<QString, QVariant>& map() { return map_; } /** * @brief load NSFSearch関数のコールバック情報を取り込む * @param pMatch 検索マッチ情報 * @param pItemTable サマリーバッファへのポインタ */ void load(const SEARCH_MATCH* const pMatch, ITEM_TABLE* pItemTable); /** * @brief 型タイプに合わせてデータを取得する * @param type 型タイプ * @param ptr データへのポインタ * @param len データ長 * @return データのQVariantオブジェクト */ static QVariant getValue(WORD type, char* ptr, USHORT len); private: SEARCH_MATCH match_; QMap<QString, QVariant> map_; }; /** * @brief サマリーバッファのリストクラス */ typedef QList<SummaryMap> SummaryList; } // namespace ntlx
ただし、今回はgetValueメソッドから数値リスト、日時リストを処理するためのテンプレート関数にフォーカスします。getValueメソッドの中で、数値リストと日時リストを呼び出す場所を抜粋します。
// summarymap.cpp抜粋 case TYPE_NUMBER: return *(NUMBER*)ptr; case TYPE_NUMBER_RANGE: return getRangeValue<Number, NUMBER, NUMBER_PAIR>(ptr, "number"); case TYPE_TIME: { TimeDate td; td.value_ = *(TIMEDATE*)ptr; return td.toQDateTime(); } break; case TYPE_TIME_RANGE: return getRangeValue<TimeDate, TIMEDATE, TIMEDATE_PAIR>(ptr, "timedate");
ptrはデータの開始ポインタを表しています。サマリーバッファにおける値の開始位置は、WORD型のデータ種別を表すので、実際の開始位置はsizeof(WORD)分だけずれます。
NUMBER型(数値)の場合は、ポインタの開始位置をNUMBER型として取得すればOKです。
TIMEDATE型(日時)の場合は、TIMEDATE構造体のポインタに置き換えればいいのですが、以前に定義したTimeDateクラスを応用して、QDateTimeオブジェクトに変換しています。
それでは、本題のRANGE(範囲)型のデータを取得してみましょう。
数値と日時範囲の取得には、関数テンプレートgetRangeValueを使うことにします。この関数は、クラス、単一データリストの型、ペアデータ(範囲データ)リストの型をパラメータに取ります。
// summarymap.cpp抜粋 template<class T, typename S, typename P> QVariant getRangeValue(char* ptr, const QString& typeName) { RANGE* pRange = (RANGE*)ptr; S* pSingle = (S*)(pRange + 1); P* pPair = (P*)(pSingle + pRange->ListEntries); ...
ポインタの最初の位置からRANGE型を取得します。ポインタにおいて、変数のサイズ分だけポインタを移動するには、データ型ポインタに1を足します(詳細はWebで)。RANGE型の次のポインタにあるのは「単一データのリスト」のスタート地点です。さらに、単一データからRANGE.ListEntries分ポインタを進めると、ペアデータ(範囲データ)のスタート地点を取得できます。
... QList<QVariant> list; for (USHORT i = 0; i < pRange->ListEntries; ++i, ++pSingle) { T item(*pSingle); list.append(item.toVariant()); } ...
最初に単一データを取得します。RANGE.ListEntries分ループを回して、Tクラスのオブジェクトを作成し、リストにそのQVariant値を追加します。TクラスとはNumberクラスかTimeDateクラスを表します。T.toVariant()メソッドについては後述します。
... QList<QVariant> pairList; for (USHORT i = 0; i < pRange->RangeEntries; ++i, ++pPair) { T lower(pPair->Lower); T upper(pPair->Upper); QMap<QString, QVariant> pair; pair.insert("lower", lower.toVariant()); pair.insert("upper", upper.toVariant()); pairList.append(pair); } ...
続いてペアデータを取得します。RANGE.RangeEntries分ループを回して、TクラスのP.Lower、P.Upperそれぞれに対応したオブジェクトを作成し、それらをQVariant値に変換してQMapオブジェクトを作成し、リストに追加します。なお、ここでQPairを使う方がシンプルですが、デフォルトではQPairをQVariantに変換できないため、ここではQMapを使用しています。
... QMap<QString, QVariant> value; value.insert("list", list); value.insert("range", pairList); value.insert("type", typeName); return value; }
最後に、取得した単一データリスト、ペアデータリストをQMapのlist、rangeのデータとして挿入します。単にこのマップデータを返しただけでは数値なのか日時なのかわからないので、最後にtypeとして型名文字列を挿入しています。
ここでさきほどのT.toVariant()メソッドについて。このメソッドはTオブジェクトからQVariantデータを取得するのが目的になりますが、実を言えば、このようなテンプレートのために設けたと言っても過言ではありません。TIMEDATEについては、TimeDateクラスで以下のように実装しています。
// timedate.cpp抜粋 QVariant TimeDate::toVariant() const { return QVariant(toQDateTime()); }
TimeDate.toQVariantは、TIMEDATE値をQVariantに対応しているQDateTimeオブジェクトに変換してQVariantを返します。
もう一方のNUMBER(数値型)ですが、今までラッパークラスを設けてきませんでした。そこで、今回C++テンプレートのためにラッパークラスを設けました。
// ntlx/number.h #ifndef NTLX_NUMBER_H #define NTLX_NUMBER_H #include "ntlx_global.h" #include "ntlx/status.h" class QVariant; namespace ntlx { /** * @brief 数値ラッパークラス */ class NTLXSHARED_EXPORT Number { public: /** * @brief デフォルトコンストラクタ */ Number(); /** * @brief コンストラクタ(NUMBER型から) * @param value NUMBER値 */ Number(const NUMBER& value); /** * @brief コピーコンストラクタ * @param other コピー元 */ Number(const Number& other); /** * @brief 代入演算子 * @param other 代入元 * @return 自身への参照 */ Number& operator=(const Number& other); /** * @brief 文字列に変換する * @return 数値文字列 */ QString toString() const; /** * @brief QVariantオブジェクトに変換する * @return QVariant値 */ QVariant toVariant() const; /** * @brief QVariant型からNumber型に変換する * @param var QVariant値 * @return Numberオブジェクト */ static Number fromVariant(const QVariant& var); private: NUMBER value_; }; } // namespace ntlx #endif // NTLX_NUMBER_H
// number.cpp #include "ntlx/number.h" #include <QVariant> namespace ntlx { Number::Number() : value_(0.) { } Number::Number(const NUMBER &value) : value_(value) { } Number::Number(const Number &other) : value_(other.value_) { } Number& Number::operator=(const Number& other) { if (this == &other) return *this; value_ = other.value_; return *this; } QString Number::toString() const { return QString::number(value_); } QVariant Number::toVariant() const { return QVariant(value_); } Number Number::fromVariant(const QVariant &var) { return Number(var.toDouble()); } } // namespace ntlx
これにより、少なくとも関数テンプレートgetRangeValueにおいては、日時型TIMEDATEと数値型NUMBERは、範囲型TIMEDATE_PAIRとNUMBER_PAIRとともに、TimeDateクラスとNumberクラスを伴って同等の扱いをすることが可能になりました。
今回は、Cポインタ、関数テンプレート、QtのQVariantといういろいろな手法を取り入れて、数値と日時の「範囲型」データの取得に挑んでみました。NTLXライブラリの追加・変更点はまだいくつかありますので、順に説明していきたいと思います。
伝説のツール「NotesPeek」をQtでリメイクする(その10・リスト型/範囲型)
Notesの基本的な「型」は、テキスト(文字列)型、数値型、日時型になります。これら基本型は、いずれも複数値になることは皆さんご存じのことと思います。
- テキストリスト型
- 数値リスト型
- 日時リスト型
このうちテキストリストは、LISTという構造体が使われています。
typedef struct { USHORT ListEntries; /* list entries following */ /* now come the list entries */ } LIST;
この図は文字列リストのメモリ配置を示していて、LIST.ListEntries=3の場合を表しています。L1〜L3はUSHORT型で、文字列1〜3の長さを表しています。
LISTが適用できるのはテキスト型で、TYPE_TEXT_LISTという型で判別します。では、数値や日時ではLIST型でなく、何が使われているのでしょう。
実は数値と日時には、単純なリストはなく、「RANGE」という構造体で複数値を表します。型の判別にはTYPE_NUMBER_RANGE、TYPE_TIME_RANGEという値でそれぞれ判別します。それではRANGE構造体を見てみましょう。
typedef struct { USHORT ListEntries; /* list entries following */ USHORT RangeEntries; /* range entries following */ /* now come the list entries */ /* now come the range entries */ } RANGE;
ListEntriesの他にRangeEntriesというメンバーが存在します。このRangeEntriesは、「範囲」リストの数を表します。
「範囲」という概念は、日時でいう「何日から何日」、「何時から何時」というものを表し、主にカレンダーや予定などで使用されます。Notes APIから見ると、日時型の範囲はTIMEDATE_PAIRという構造体で表されます。
typedef struct { /* a timedate range entry */ TIMEDATE Lower; TIMEDATE Upper; } TIMEDATE_PAIR;
RangeEntriesは、このTIMEDATE_PAIR型のリストのアイテム数を表していたんですね。一方で数値の方は、NUMBER_PAIRという構造体があります。
typedef struct { /* a float range entry */ NUMBER Lower; NUMBER Upper; } NUMBER_PAIR;
このNUMBER_PAIRですが、Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングによると「Notesクライアントからこれを入力することはできない」と書かれています。この本はNotes R5の頃に書かれていますが、現在のNotes 9でも数値範囲を入力できないのであれば、利用することはあまりないでしょう。ですが、APIでなら操作できるのであれば、扱えるようにしておこうと思います。
TYPE_NUMBER_RANGE、TYPE_TIME_RANGEのデータ構造は以下のようになります。
せっかく数値のRANGEと日時のRANGEが同じ構造をしているので、C++テンプレートの機能を使って、実装してみることにしましょう。(続く)
伝説のツール「NotesPeek」をQtでリメイクする(その9・UI見直し)
今回はUIの見直しです。
前回まではツリー構造のビューにひたすら追加情報を列として表示していましたが、いろいろな情報が同じ列に表示していては必ず破綻するので、今のうちに、アイテムの情報を別のペインにしておきます。
Mac版
ntlxライブラリのソースコードリポジトリはこちらです。タグ「v0.1.4」をチェックアウトしてください。
https://tkondoh2@bitbucket.org/tkondoh2/ntlx.git
NSFinder本体のソースコードリポジトリはこちらです。タグ「v0.9.3」をチェックアウトしてください。
https://tkondoh2@bitbucket.org/tkondoh2/nsfinder.git
今回は画面とソースコードの紹介にとどめ、次回以降に今回のUI見直しについて、ポイントをご紹介していきます。