伝説のツール「NotesPeek」をQtでリメイクする(その5・@式その3)

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

「ウィンドウタイトル」はフォームという設計文書に含まれる、「@式型」のフィールド(アイテム)です。ここでは、NoteクラスのgetWindowTitle()というメソッドとして定義します。このメソッドの実装プロセスを、APIを中心にしてみていきます。

QString ntlx::Note::getWindowTitle();

このgetWindowTitleメソッドは、以下のように4つのプロセスで成り立ちます。

QString ntlx::Note::getWindowTitle()
{
  QString formName = getShortText(FIELD_FORM);
  Note formNote = db_->getDesignNote(formName, NOTE_CLASS_FORM);
  Formula formula = formNote.getFormula(ITEM_NAME_WINDOWTITLE);
  return formula.evaluate(*this);
}
  1. 自身(文書)が持っているFormフィールドの値(=フォーム名)を取得します。
  2. フォーム名に該当するフォーム設計文書を取得します。
  3. フォーム設計文書が持つウィンドウタイトルフィールドの値(=コンパイル済み@式、バイナリ)を取得します。
  4. 自身(文書)を基準に@ウィンドウタイトル式を評価し、その文字列を返します。

ここでは、最後の「式の評価」プロセスを順に説明しますが、その前に少しだけ、getFormula()内で起きるバイナリデータの扱いについて触れておきます。

取得したコンパイル済み@式をすぐに評価する場合は必要ないと思いますが、例えばこのgetFormula()のように、文書(Note)から取得したコンパイル済み@式をFormulaオブジェクト内で保持して戻り値とする必要がある場合、取得したコンパイル済み@式(バイナリ)と同じ状態でコピーする必要があります。手順としては以下のようになります。

  1. NSFItemInfo関数で取得した値用ブロックID(=コンパイル済み@式)を、OSLockBlockでロックし、ポインタを得る(コピー元)。
  2. OSMemAllocでメモリハンドルを取得し、OSLockObjectでロックし、ポインタを得る(格納先)。
  3. コピー元から格納先に必要な長さ分メモリコピーをする。
  4. 格納先をOSUnlockObjectでアンロックする。
  5. コピー元をOSUnlockBlockでアンロックする。

ユーザ側がOSMemAllocで割り当てたメモリは、責任を持って不要時にOSMemFreeで解放します。一方で、NSFItemInfo関数で得たブロックIDは、責務はAPI側にあるので、ユーザ側で解放してはいけません。

話を「式の評価」に戻します。

@式を評価する手順は大まかに次の通りです。

  1. コンパイル済み@式をロックし、ポインタを得る(式ポインタ)。
  2. NSFComputeStart()関数に式ポインタを与えて、計算ハンドル(HCOMPUTE型)を得る。
  3. 基準とする文書(Document)を開いて、文書ハンドル(NOTEHANDLE型)を得る。
  4. NSFComputeEvaluate()関数に計算ハンドル、文書ハンドルを与えて、結果ハンドル(DHANDLE型)を得る。
  5. 結果ハンドルをロックすると、結果がLMBCS文字列として取得できる。
  6. 結果ハンドルは不要になったらアンロックし、ユーザ側の責務で解放する。

このように@式は、式ハンドル→式ポインタ→計算ハンドル→結果ハンドルというように、いくつもハンドルが出てくるのが特徴です。 計算ハンドルは使い回すことができるので、式の内容が同じなら計算ハンドルをキープし、NSFComputeEvaluateを繰り返し呼び出した方が効率がよくなるでしょう。不要になった計算ハンドルはNSFComputeStopでリリースします。

@式を利用する重要な手順には、これらの他に「コンパイルする」という手順が加わります。今回扱っている「ウィンドウタイトル」は、Domino Designerを通してコンパイルされて、フォーム設計文書に格納されているため説明を省きました。テキストの@式をコンパイルしてバイナリにする「NSFFormulaCompile」の他に、コンパイル済みの@式をテキストにする(デコンパイルする)API「NSFFormulaDecompile」もありますが、いずれ必要に応じて説明したいと思います。

伝説のツール「NotesPeek」をQtでリメイクする(その5・@式その2)

少し時間が空いたので、前回のおさらいをしておきます。

  1. @式(または@関数)はR4以前からある操作言語である。
  2. フォームは文書(Document)設計用の文書(Note)である。
  3. ウィンドウタイトルは、フォーム専用のフィールドである。
  4. @式は文字列として書かれ、バイナリとして保存される。

おおざっぱに言えば、このような感じです。

今回のリメイクツールでは、データベースのツリーアイテムを展開すると、すべての文書(Document)を取得して子アイテムとしてデータベースアイテムにぶら下がります。そして、各文書アイテムでは、NotesPeekのように、ウィンドウタイトルを表示させることにします。

ちなみに、NotesPeekではこのように文書(Document)を表示しています。

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

もし表示されない場合は、メニュー「View−Options」としてこのダイアログを表示し、左ペインで「Notes」を選択し、右ペインで「Title」にチェックを付けると出てきます。

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

この「Title」として表示されるのが「ウィンドウタイトル」になります。

前回も触れましたが、このウィンドウタイトルを取得するまでの手順もおさらいします。

  1. 文書(Document)を取得する。
  2. その文書の「Form」フィールドのテキスト値(フォーム名)を取得する。
  3. フォーム名に該当するフォーム(設計文書)を取得する。
  4. フォームの「ウィンドウタイトル」フィールドの値(=@式、バイナリ形式)を取得する。
  5. 文書(Document)を引数にして、ウィンドウタイトル式を評価(Evaluate)する。

それでは、順に関連するAPIを紹介していきます。文書の取得、テキスト値の取得は既出なので省略します。

それでは最初にフォーム文書(フォームNote)の取得です。フォームを含む設計文書の取得には、NIFFindDesignNoteを使用します。

#include <nif.h>

STATUS LNPUBLIC NIFFindDesignNote (DBHANDLE hFile, const char far *Name, WORD Class, NOTEID far *retNoteID);

hFileにデータベースハンドル、Nameに設計要素名、Classに文書クラスを与え、、retNoteIDに該当する設計要素文書のNoteIDが返ってきます。

次に、文書から@式を含む任意のフィールド値を取り出すには、NSFItemInfo関数を使います。

#include <nsfnote.h>

STATUS LNPUBLIC NSFItemInfo (
    NOTEHANDLE hNote
    , const char far *Name
    , WORD NameLength
    , BLOCKID far *retbhItem
    , WORD far *retDataType
    , BLOCKID far *retbhValue
    , DWORD far *retValueLength);

入力値として、hNoteに文書(Note)ハンドル、Nameにフィールド名、NameLengthにフィールド名の長さを指定します。

出力値としては、retbhItemにアイテム用のブロックID、retDataTypeにアイテムの型、retbhValueにアイテム値用のブロックID、retValueLengthにその長さが設定されます。

BLOCKIDとは、端的に言えば、文書内に格納されたデータへのハンドルです。@式の場合、アイテム値用のブロックIDをロックして、@式のバイナリデータを取得できます。ロックにはOSLockBlock、アンロックにはOSUnlockBlockを使います。

#include <pool.h>

#define OSLockBlock(type,blockid) ((type far *)(OSLock(char,(blockid).pool) + (blockid).block))

#define OSUnlockBlock(blockid) OSUnlockObject((blockid).pool)

取得したバイナリデータは、自前で用意したメモリに格納しておきます。自前でメモリを用意するにはOSMemAlloc、用意したメモリを解放するにはOSMemFreeを使います。メモリはハンドル形式なので、メモリ内を使う場合はブロックID同様ロックして使いますが、微妙に仕様が異なります。

#include <osmem.h>

STATUS LNPUBLIC OSMemAlloc (WORD BlkType, DWORD dwSize, DHANDLE far *retHandle);
STATUS LNPUBLIC OSMemFree (DHANDLE Handle);

void far * LNPUBLIC OSLockObject (DHANDLE Handle);
#define OSLock(blocktype,handle) ((blocktype far *) OSLockObject(handle))
#define OSUnlock(handle) OSUnlockObject(handle)
BOOL LNPUBLIC OSUnlockObject (DHANDLE Handle);

次回は@式に関するAPIを説明します。

伝説のツール「NotesPeek」をQtでリメイクする(その5・@式その1)

@式、または@関数は、NotesがR4でLotusScriptを実装するまで、唯一の操作言語でした。@式はあらゆるところで使えます。フィールドの計算式、列式、UI操作(@Command)、Webの時代が来ても、CGIのような使い方ができたりしました。そんな@式は、API側から見るとどのようになっているのでしょう。NotesPeekリメイクプロジェクトにおいて、最初に出会うのはフォームのウィンドウタイトルになるので、これを中心に@式を見ていきます。

フォームは、文書です。

役割を省くと、フォームは文書(Note)に分類されます。データを保存するのか、文書のための設計要素を保存するかで、Noteは文書かフォームかに分けられます。Noteには「クラス」という属性値があり、文書はNOTE_CLASS_DOCUMENT、フォームはNOTE_CLASS_FORMとなります。厳密に言えば、NOTE_CLASS_FORMはフォーム以外の設計要素にも用いられています。初期のNotesはそれほど設計要素を持っていませんでしたが、バージョンを重ねていろいろな要望を取り入れる中で、クラスとしてはフォームでも、ある属性値を持って細分化されていきました。細分化された設計要素については、後日説明する機会があると思います。

ウィンドウタイトルは、フィールドです。

Note上に格納されるあらゆるものが、属性を除けばフィールドということになります。ウィンドウタイトルは、フォームというNote専用のフィールドになります。フィールドにもいろいろなタイプがあります。テキスト、数値、日時、リッチテキストなどがそうです。ウィンドウタイトルは何タイプなのか。これは「@式」型になります。Domino Designerを使って普通にフォームを作成する分には作ることのできないフィールド型です。

@式は、バイナリです。

@式は、バイナリになります。テキストとしての@式(文字列型)は、コンパイルすることでバイナリの@式になります。逆にバイナリの@式をデコンパイルするとテキストの@式が得られます。@式を評価する(@式を計算して値を得る)場合は、バイナリの@式から計算ハンドルというものを取得して、計算をさせます。通常、@式は文書をベースに計算します。フィールドの計算結果や、ビューの列式も、原則文書単位で計算されます。

話をウィンドウタイトルに戻します。フォームのウィンドウタイトルは、文字通り現在開いている文書情報を元に、ウィンドウやタブのタイトルとして表示するための@式です。フォームNoteの中には、フィールド名「$WindowTitle」の@式型(コンパイル済みのバイナリ)として格納されます。

なお、本プロジェクトでは、コンパイル済みの@式を使うので、当面はコンパイル(テキストからバイナリ)のプロセスは含めずに進めていきます。 (@式その2に続く)

伝説のツール「NotesPeek」をQtでリメイクする(その4・IDテーブル)

IDテーブル

IDテーブルは、Notesにおける文書のためのコンテナの役割を持っています。Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングでも書かれていますが、

文書IDテーブルとは、文書ID(NOTEID)を昇順で保持するコンテナで、std::setとほぼ同じ機能を提供します。

と、あります。もともとAPIはC用に作られているので、C++のコンテナをそのまま使うわけにはいきません。CのみであればこのIDテーブルを使えばいいわけです。またC++であればstd::setを使ってみても問題ありません。ただ、著者の津田氏も勧めているように、IDテーブルの仕組みの理解と、C++のような実装をする勉強のためにも、IDテーブルを取り込んだ、C++っぽいクラスを作ってみます。津田氏の場合、NoteIdCollectionクラスとそのイテレータクラスで構成されています。9割以上は氏のコーディングを参考にしています。ただ、あまりNoteクラスと密な関係にせず、基本的にはNOTEIDの出し入れというシンプルな構造に変えてみました。

まず、IDテーブルクラスの実装を紹介する前に、ここで扱うIDテーブル関連のAPIを紹介します。

IDテーブルは、IDCreateTable関数によって生成し、IDDestroyTable関数によって破棄します(例外もあります)。生成されたIDテーブルはDHANDLEにて管理します。

#include <idtable.h>

STATUS LNPUBLIC IDCreateTable (DWORD Alignment, DHANDLE far *rethTable);
STATUS LNPUBLIC IDDestroyTable(DHANDLE hTable);

ここで、Alignmentはsizeof(NOTEID)を渡すことになります。

IDテーブルにIDを追加するには、IDInsert関数を使います。

#include <idtable.h>

STATUS  LNPUBLIC IDInsert (DHANDLE hTable, DWORD id, BOOL far *retfInserted);

戻り値はいつものステータス値ですが、IDが挿入できたかどうかは3番目の引数、retfInsertedを見ます。これは、IDがすでにIDテーブル内に存在していればfalseを返します。必要なければnullptrを与えておきます。

テーブル内のIDは通常IDDeleteかIDDeleteAllを使って削除しますが、その実装は後日とします。また、Noteクラスとの連携に唯一getNoteメソッドを実装していますが、これも後日説明します。

それでは、僕流のIDTableクラスと、IDTable::iteratorクラスです。

// ntlx/idtable.h

#ifndef NTLX_IDTABLE_H
#define NTLX_IDTABLE_H

#include "ntlx_global.h"
#include "ntlx/status.h"

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

#include <nsfdata.h>

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

namespace ntlx
{

class Database;
class Note;

/**
 * @brief IDテーブルクラス
 */
class NTLXSHARED_EXPORT IDTable
    : public IStatus
{
public:

  /**
   * @brief イテレータインナークラス
   */
  class NTLXSHARED_EXPORT iterator
  {
  public:
    /**
     * @brief デフォルトコンストラクタ
     */
    iterator();

    /**
     * @brief NOTEIDを返す演算子
     * @return NOTEID
     */
    NOTEID operator*();

    /**
     * @brief 前置きインクリメント演算子
     * @return 自身への参照
     */
    iterator& operator++();

    /**
     * @brief 後置きインクリメント演算子
     * @return インクリメントする前のイテレータ
     */
    iterator operator++(int);

    /**
     * @brief NOTEIDに対応したNoteオブジェクトを取得する
     * @param db 取得元になるデータベースオブジェクト
     * @return Noteオブジェクト
     */
    Note getNote(Database& db) const;

  protected:
    /**
     * @brief コンストラクタ
     * @param pIdTable IDTableオブジェクトへのポインタ
     * @param noteId NOTEID
     * @param isLast 最後を示しているか
     */
    iterator(IDTable* pIdTable, NOTEID noteId, bool isLast = false);

    IDTable* idTable_;
    NOTEID id_;
    bool isLast_;

    /**
     * @brief 等値演算子
     * @param lhs 左辺値
     * @param rhs 右辺値
     * @return ブール値
     */
    friend NTLXSHARED_EXPORT bool operator==(
        const iterator& lhs
        , const iterator& rhs
        );

    /**
     * @brief 不等値演算子
     * @param lhs 左辺値
     * @param rhs 右辺値
     * @return ブール値
     */
    friend NTLXSHARED_EXPORT bool operator!=(
        const iterator& lhs
        , const iterator& rhs
        );

    friend class IDTable;
  };

  /**
   * @brief コンストラクタ
   */
  IDTable();

  /**
   * @brief デストラクタ
   */
  virtual ~IDTable();

  /**
   * @brief 最初のイテレータを返す
   * @return 最初のイテレータ
   */
  iterator begin();

  /**
   * @brief 最後のイテレータを返す
   * @return 最後のイテレータ
   */
  iterator end();

  /**
   * @brief NOTEIDを挿入する
   * @param id NOTEID
   * @return 重複していなければ真
   */
  bool insert(NOTEID id);

private:
  DHANDLE handle_;

  friend class iterator;
};

} // namespace ntlx

#endif // IDTABLE_H
// idtable.cpp

#include "ntlx/idtable.h"
#include "ntlx/note.h"

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

#include <idtable.h>

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

namespace ntlx
{

IDTable::iterator::iterator()
  : idTable_(nullptr)
  , id_(0)
  , isLast_(false)
{
}

NOTEID IDTable::iterator::operator *()
{
  return id_;
}

IDTable::iterator& IDTable::iterator::operator++()
{
  isLast_ = !IDScan(idTable_->handle_, id_ == 0, &id_);
  return *this;
}

IDTable::iterator IDTable::iterator::operator++(int)
{
  iterator it = *this;
  operator++();
  return it;
}

Note IDTable::iterator::getNote(Database &db) const
{
  return Note(db, id_);
}

IDTable::iterator::iterator(IDTable *pIdTable, NOTEID noteId, bool isLast)
  : idTable_(pIdTable)
  , id_(noteId)
  , isLast_(isLast)
{
}

bool operator==(const IDTable::iterator& lhs, const IDTable::iterator& rhs)
{
  if (lhs.isLast_ && rhs.isLast_)
    return true;
  else
    return lhs.id_ == rhs.id_;
}

bool operator!=(const IDTable::iterator& lhs, const IDTable::iterator& rhs)
{
  return !operator==(lhs, rhs);
}

IDTable::IDTable()
  : IStatus()
  , handle_(NULLHANDLE)
{
  lastStatus_ = IDCreateTable(sizeof(NOTEID), &handle_);
}

IDTable::~IDTable()
{
  if (handle_)
    IDDestroyTable(handle_);
}

IDTable::iterator IDTable::begin()
{
  if (handle_ == NULLHANDLE)
    return end();
  return ++iterator(this, 0);
}

IDTable::iterator IDTable::end()
{
  return iterator(this, 0, true);
}

bool IDTable::insert(NOTEID id)
{
  Q_ASSERT(handle_);

  BOOL b = false;
  lastStatus_ = IDInsert(handle_, id, &b);
  return b;
}

} // namespace ntlx

Domino Web Server APIで自作Web APIは作ることはできるのか?プロローグ

Domino Web Server API、略してDSAPI。いわゆるHTTPタスクのアドインです。HTTPタスクに介入できるようになったのは、R5からだったと記憶しています。

昔からDSAPIで標準提供されているものに、Domino Offline Service(DOLS)がありますし、DSAPIではないですが、iNotesやXPagesなどもDSAPIを起点にしてHTTPタスクに統合されていったのではないかと勝手に想像しています。

Domino Data Service(DDS)は、DominoサーバをREST/WebAPI化する強力なサービスで、ググると8.5.3 SP1からの提供であるようですが、これ以前に、自前でRESTサービスをDominoに組み込めないか、よく思案したものです。

DDSはJSONでレスポンスを返すので、Node.jsやJavaScriptフロントエンドなどと相性もよく、標準のデータベースアクセスのほかにカレンダーに特化したものもあり、機能的にはそんなに遜色ない出来だと思っています。

でも、使い込んでくると欲が出てくるもので、「ああ!こんな機能があったらいいのに!!!」と思ったことは一度や二度ではなかったですね。

そんなわけで、これまた昔からの小さな夢を、実現してみようかと思い立ち、自作Web APIを作ってみることにしました。今回はそのプロローグ、Hello,Worldです。

OSとDominoはWindows、ライブラリはVS2013 32bitとQt5.6、Dominoは9.0.1です。VSモジュール(msvcr120.dll、msvcp120.dll)とQtCoreのDLLを、パスで解決できる場所に置きます。DSAPIを使ったアドインDLLは、他のアドインと違って序数1によるエキスポートは不要ですが、Cリンケージ(extern “C”)にしておく必要があります。

DSAPIは、関数やシンボルなどを「FilterXXXX」と命名しているように、基本的にHTTP処理のフィルタリングを主目的としているようです。処理できる段階は、リクエストの受付時からレスポンスの返しまでの一般的なものから、認証処理時、名前リスト構築時など、認証機能を備えたDominoならではのタイミングもあります。今回は、認証済みのタイミング(kFilterAuthorized)を使ってみました。

Webブラウザなどから、http://domino.server/csapi/とたたくと、Hello~とプレーンテキストで返すだけの単純なサンプルは、以下の通りです。

main.cpp

#include <QByteArray>

#pragma pack(push, 1)
#include <global.h>
#include <dsapi.h>
#include <addin.h>
#pragma pack(pop)

extern "C"
__declspec(dllexport) uint FilterInit(FilterInitData* pInitData)
{
  pInitData->appFilterVersion = kInterfaceVersion;
  pInitData->eventFlags = kFilterAuthorized;
  strcpy(pInitData->filterDesc, "Chiburu Systems API");
  AddInLogMessageText("Chiburu Systems API Started.", NOERROR);
  return kFilterHandledEvent;
}

extern "C"
__declspec(dllexport) uint HttpFilterProc(
    FilterContext* ctx
    , uint eventType
    , void* /*ptr*/
    )
{
  uint errID;
  switch (eventType)
  {
  case kFilterAuthorized:
  {
    FilterRequest request;
    ctx->GetRequest(ctx, &request, &errID);
    QByteArray url(request.URL);
    if (url.startsWith("/csapi/") && request.method == kRequestGET)
    {
      QByteArray buffer(request.version);
      buffer += " 200 OK\r\n";
      buffer += "Content-Type: text/plain\r\n";
      buffer += "\r\n";
      buffer += "Hello, Chiburu Systems Web API!\r\n";
      ctx->WriteClient(ctx, buffer.data(), buffer.size(), 0, &errID);
      return kFilterHandledEvent;
    }
  }
    break;
  }
  return kFilterNotHandled;
}

訂正(2017/4/10)

誤:Domino Server API => 正:Domino Web Server API 誤:\n => 正:\r\n

IBM Notes C APIのMacにおけるDWORD問題

現在、このブログでQtによるNotesPeekリメイクプロジェクトを進めていますが、ここで今日、問題が発生しました。

Notesデータベースから文書を取得する際にNSFSearch関数を使用し、NOTEID型を取得しようとしました。

WindowsLinux(Ubuntu)では問題は出ませんでしたが、Mac環境において、正確にNOTEIDが取得できませんでした。

NOTEIDはDWORD型です。APIにおいてDWORDは、32bit幅の符号なし整数になります。この定義は、APIのglobal.hヘッダーファイルに含まれています。

Win32やUNIX(LINUX)においては問題が発生していません(Domino Serverを対象にしたWin64の場合は検証できていません)。

しかし、Macコンパイラにおいて、他のOSと違ってDWORDの幅が異なりました。

APIに掲載されているオプションもいろいろ試しましたが、今のところ、解決できた追加オプションは以下の通りです。

DEFINES += LONGIS64BIT

何かわかり次第、補足したいと思います。

伝説のツール「NotesPeek」をQtでリメイクする(その3・その2の補足)

ここでは、その2で紹介したコードについて、ファイル検索以外の点について補足します。

ntlxライブラリ

QString Status::toMessage()

StatusクラスにtoMessageメソッドを追加しました。以前は、

QString msg = Lmbcs(status).toQString();

としていましたが、

QString msg = status.toMessage();

と、Status単独でステータス値からメッセージを取得することができます。

IStatusクラス

メソッドとしては値を返り値にしたいが、関数が処理したステータスも重要な場合、いちいち引数にStatusのポインタを置くのも面倒です。そんな時、クラスにこのインターフェースクラスを多重継承します。メソッド内部で取得したステータスをメンバ変数lastStatus_に預けることで、メソッドから戻ってきてもステータス値を失うことなく処理を継続できます。

initEx関数、term関数

NotesInitExtended関数、NotesTerm関数のラッパー関数です。

PathSetクラス、NetPathクラス

Notesデータベースへのパスに使用するクラスです。PathSetは、パス、サーバ名、ポート名を一度に扱えるクラスです。NetPathはLmbcsクラスを継承して、Notes状のフルパスである「ネットパス」に対応しています。Databaseクラスを使って、データベースをオープンする場合、次のように書けます。

Database db1(PathSet("names.nsf", "Server1")); // PathSet使用
Database db2(PathSet("names.nsf", "Server2").toNetPath()); // NetPathに変換してもOK

PathSetからNetPathへはPathSet::toNetPathを使用しますが、NetPathからPathSetにする場合はNetPath::toPathSetを使うことができます。

DbInfoクラス

データベースのタイトルやカテゴリを取得する場合、Lmbcsから継承したDbInfoクラスを使用します。データベース情報はタイトル、カテゴリ、DBクラス、設計元DBクラスの4つの情報を有していますが、実体は128バイトしかないLMBCS文字列です。そこで、この文字列をDbInfo内に置き、メソッドによってタイトルなどの各情報を取得することができるようにしました。以前はDatabaseクラスに組み込まれていましたが、NSFSearch関数によって取得したサマリーバッファでも、データベース情報のパースが必要になることがわかったので、Databaseクラスから切り離し、DbInfoクラスとして独立させることにしました。