2019年8月13日火曜日

JSON Pointer に翻弄される

前回のエントリ「JSON から目的の値を取り出す」では NotesJSONNavigator クラスの getElementByPointer メソッドを使った例をロクな説明もなしに示しました。

私が翻弄されたのは、JSONのデータに「配列」と「連想配列」が混在することからくるものでした。

この「配列」は、
[ "v1", "v2", "v3" ]
のように「値」を大括弧 [ ] で囲みます。

もうひとつの「連想配列」はというと
{ "k1": "v1", "k2": "v2", "k3": "v3" }
のように「キー」と「値」のペアを中括弧 { } で囲みます。
キーと値のペアで配列を作るところは、LotusScript で例えるなら「リスト」に似ていると思います。


そして getElementByPonter メソッドを使うと、JSON 上の位置を Pointer に指定することで効率よく目的の Element から「値」を取り出すことができます。

実際のコードを示します。
※以下の環境は Community版の Notes 10.0.1 FP2 です

まずは次のような Function を用意しました。
Function getValue( json$, pointer$ ) As Variant
 Dim ss As New NotesSession
 Dim nav As NotesJSONNavigator
 Dim elm As NotesJSONElement
 
 Set nav = ss.Createjsonnavigator( json )
 Set elm = nav.Getelementbypointer( pointer )
 Select Case elm.Type
  Case 1, 2 '1: Object, 2: Array
   MessageBox "Element type is Object or Array"
  Case 3, 4, 5, 64 '3: String, 4: Number, 5: Boolean, 64: Empty
   getValue = elm.Value
  Case Else
   messagebox "undefined type"
 End Select
End Function

配列にある2番目の値を取り出す場合、Pointer に "/1" を指定します。
JSON = |[ "v1", "v2", "v3" ]|
pointer = "/1"
Print getValue( JSON, pointer )

配列の最初は 0、2番目は 1、n番目は n-1 といった数字をスラッシュ"/"の右側に指定します。

連想配列では、キー "k2" の値を取り出す場合、Pointer に "/k2" を指定します。
JSON = |{ "k1": "v1", "k2": "v2", "k3": "v3" }|
pointer = "/k2"
Print getValue( JSON, pointer )

連想配列は、キーを人間が理解しやすい文言にしておくことで、値が取り出しやすくなります。


さて、実際の JSON では、連想配列の「値」の部分が「配列」になっている、といったように、連想配列と配列とが交互に現れたりする場合があります。

これらを区別しながら、連想配列 { } の場合は「キー」を、配列 [] の場合は「位置」を、それぞれ正しく指定しなければなりません。

前回のエントリでも示した次の JSON を見ていきます。
{
  "responses": [
    {
      "textAnnotations": [
        {
          "locale": "ja",
          "description": "取り出したいテキストは\nここにあります!\n",
          "boundingPoly": {
            "vertices": [・・・
            ]
          }
        }
      ]
    }
  ]
}

ここから "description" の「値」を取り出したいので、

  1. 連想配列にあるキー"responses" の値(配列)の、
  2. その配列の最初 "0" の、
  3. 連想配列にあるキー "textAnnotations" の値(配列)の、
  4. その配列の最初 "0" の、
  5. 連想配列にあるキー "description"

という構造になるよう Pointer を組み立てると、次のようになります。
/responses/0/textAnnotations/0/description


ところで Pointer では、連想配列の「キー」や配列の「位置」をスラッシュ"/"で区切っていますが、「キー」にスラッシュ"/"を含む場合はどうしたら?と疑問がわきました。

「きっとバックスラッシュでエスケープするんだろうな」などと安易に試したら見事に外れてしまい...

実はこれ RFC 6901 で仕様化されていました。

スラッシュ "/" とチルダ "~" は特別な意味があることから、
"/" は "~1"
"~" は "~0"
に符号化するのだそうです。

ということで、次のように キーが "Key/0" の場合、Pointer は "/key~10" とするのが正解でした。
JSON = |{ "key/0": false, "Key/1", true }|
pointer = "/key~10"
Print getValue( JSON, pointer )



以下におまけとして、(私にとって?)イレギュラーだったケースを以下に示します。

JSON文字列が(指定できることが正しいかどうかはさておき)配列や連想配列ではない場合、Pointer を "" とすると取れました。
JSON = |"v1"|
pointer = ""
Print getValue( JSON, pointer )

連想配列の「キー」が日本語の場合、Pointer として日本語文字列を指定しても not found (コード 4843)となりました。
JSON = |["キー1", "値1", "キー2", "値2"]|
pointer = "キー2"
Print getValue( JSON, pointer )


「値」が真偽値( True / False )の場合、True / False の最初の文字が小文字になっていないと、 JSON を JSONNavigator へロードしたところでパースできないことを示すエラー(コード 4842)となりました。
JSON = |["1", 2, True]| 'true でないとエラーになる
pointer = "/2"
Print getValue( JSON, pointer )


それから、連想配列や配列の最後に余計なカンマがある場合もパースできないと叱られてしまいました...
文法は厳しくチェックされるようです。

2019年8月10日土曜日

JSON から目的の値を取り出す

画像に含まれるテキストをOCRで抽出するサービスが Google にあることを IBM Champion の小野様に教えていただきました。 


ものは試しにと LotusScript のエージェントでチャレンジしてみたのですが、不慣れな JSON に手間取ってしまったので、ここにメモとして残しておこうと思った次第です。


作成したエージェントの概要ですが、NotesStream に画像ファイルを読み込み Base64 エンコードしたものを、リクエストのボディとして渡す JSON の一部に埋め込み、NotesHTTPRequest を使って指定のエンドポイント(URL)へ JSON を POST します。返ってきた JSON から OCR で取得できたテキストを取り出して表示します。

このエージェントの中で OCR の結果が JSON 形式で戻るのですが、これが私にとってなかなか分かりづらいものでした。 戻ってきた JSON は(実際には2121行もあったのでほぼ省略しますが)次のようになっていました。
{
  "responses": [
    {
      "textAnnotations": [
        {
          "locale": "ja",
          "description": "取り出したいテキストは\nここにあります!\n",
          "boundingPoly": {
            "vertices": [・・・
            ]
          }
        },・・・
      ]
    }
  ]
}

実際に戻ってきた JSON データには、OCR で抽出できた「テキストの全体」を示すものと、「テキストの一部」を示すものがありました。今回取り出したいのはテキストの全体を示すほうです。

そして「テキスト全体」がJSONデータの中に、どうやら2か所にあることがわかりました。どちらを採用するのが正しいのかはさておき、今回はJSONデータの冒頭に見つけた、次の位置の文字列を取り出すことにしました。

「配列 "responses" の最初のエレメントである配列 "textAnnotations" の最初のエレメントにある "description"」


まずは戻ってきた JSON を NotesJSONNavigator などの V10 で追加されたJSONを処理するためのクラス群を使って目的の値を取り出します。

まず私が一つ目の値の取り込みに使用したのは次のようなコードです。変数"JSON"に上記JOSN文字列がセットされてます。

Dim ss As New NotesSession
Dim nav As NotesJSONNavigator
Dim elm As NotesJSONElement
Dim obj As NotesJSONObject
Dim arr As NotesJSONArray

Set nav = ss.CreateJSONNavigator(JSON)
Set elm = nav.Getelementbyname("responses")
Set arr = elm.Value
Set elm = arr.Getfirstelement()
Set obj = elm.Value
Set elm = obj.Getelementbyname("textAnnotations")
Set arr = elm.Value
Set elm = arr.Getfirstelement()
Set obj = elm.Value
Set elm = obj.Getelementbyname("description")
Print elm.Value


取り込みたい JSONObject の名前や配列上の位置が決まっていることが前提なので JSONElement の Type を確認することなくこのようなコードにしています。

位置が「決まっている」とはいえ、少々面倒くさいというか冗長なコードだなという印象です。


ところが、先ほどこちらの資料を読んでいたら、もっとシンプルなコードで実現できることがわかりました。

Dim ss As New NotesSession
Dim nav As NotesJSONNavigator

Set nav = ss.CreateJSONNavigator(JSON)
Set elm = nav.Getelementbypointer( "/responses/0/textAnnotations/0/description" )
Print elm.Value


NotesJSONNavigator クラスの GetElementByPointer メソッドを使うことで、ずいぶんとシンプルになりました。
このとき「配列の最初」を示す数値は「0」となります。

2019年8月9日金曜日

ビューの「索引」

昨晩は「のの会」に参加しました。

実は先月の「のの会」でお話したネタが時間切れのため約半分を次回持越しとしました。
それで持ち越した残り半分を、昨晩お話しました。

そのネタとは「ビュー索引」です。

Notes/Domino の世界で索引と言えば、「全文索引」と「ビュー索引」の2つありますが、今回はビュー索引にスポットをあててみました。

ビュー索引は、パフォーマンスに影響することがしばしばあります。そのため、ビュー索引を維持管理する仕組みには様々な工夫があり、機能改善が続けられています。

お話しながら見ていただいた資料は(のの会でのお話にあわせて)前半/後半に分けて SlideShare へアップしました。

前半は、

  • ビューの構造
  • 索引の維持に関連するビューの設定
  • インデクサ

後半は、

  • 索引を維持するタスクと調整するオプション
  • バージョン9,10における新機能
  • パフォーマンスに係る状況の確認の方法

といった内容となっています。

基本的には、メーカー様のWebサイトの情報やヘルプを読んだ上で、私の理解を書き出したものであり、決してメーカー様の校閲を受けたわけではありません。そのため、もしかしたら内容に誤りがあるかもしれません。内容への自身の無さの表れとしてネタ元へのリンク(IBM様のサイトためそのうちリンク切れするかもですが...)を付けていますので、ネタ元もあわせてご確認ください。





2019年7月26日金曜日

Domino の CORS 対応

CORS (Cross-Origin Resource Sharing)とは、ブラウザがHTMLを読み込んだサーバー(オリジン)以外のサーバーのデータへのアクセスを可能にするための仕組みのことです。

たとえば、node.js アプリを実行中のサーバーへブラウザでアクセスすると、アプリが HTML を返します。そのHTMLに書かれた JavaScript が(オリジンが異なる)Domino サーバー上のデータへ REST API でアクセスしようとします。この時 Domino 側で node.js アプリからのアクセスを許可する設定を行なっていない場合、ブラウザ側でブロックする、というものです。

実際このような構成で、何も対策していない Domino を使ってテストしてみたところ 401 Unauthorized となりました。
※上の画像でメソッドが GET でなく OPTIONS になっていますが、これはプリフライト(サーバーから対応するメソッドの一覧を収集すること)によるものと思います

今回テスト環境として用意した node.js 側のアプリ「app.js」と、そこから呼び出す「index.html」の内容は次のとおりです。

「app.js」の内容
const app = require('express')();
const http = require('http').Server(app);

http.listen(3000, () => console.log('Server started'))

app.get('/', function(req, res) {
    res.sendFile(__dirname + '/index.html')
})


「index.html」の内容
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>10.0.1 FP2 CORS Test</title>
    <script type="text/javascript">
 function firstscript() {
      var url = 'http://localhost/statpub.nsf/api/data/collections';
      var req = new XMLHttpRequest();
      req.open( 'GET', url);
      req.onreadystatechange = function() {
        if(req.readyState === 4 && req.status === 200) {
          var element = document.getElementById('corstest');
          element.innerHTML = '<pre>' + req.responseText + '</pre>';
        }
      };
      var authBasic = window.btoa('testuser:password');
      req.setRequestHeader('Authorization', 'Basic ' + authBasic);
      req.send();
    }
    </script>
  </head>
  <body onload="firstscript();">
    <div id="corstest">test</div>
  </body>
</html>


node.js のアプリは ポート 3000番を指定しています。Domino の HTTP タスクはデフォルトのままですので 80 番が使用されます。
どちらも同じPC上で稼働しているため、ホストは localhost です。
しかしながらオリジンは「スキーム(http://)」、「ホスト(localhost)」、「ポート」の組み合わせですので node.js の http://localhost:3000 と Domino の http://localhost は同一オリジンとはなりません。


そこで、ブラウザが CORS によりブロックしないよう、Domino 側で CORS への対応を実施することが必要になります。

CORS への対応とは、許可する「オリジン」や「メソッド」等を設定することです。

Domino へアクセスした時、Domino はオリジンやメソッド等が許可されたものであるかを確認します。それらが許可しているものであれば、許可していることを示すヘッダー情報をブラウザへ返します。


さて、Domino のオンラインヘルプによれば、Domino 10.0.1 Fix Pack 2 以降で CORS に対応する、とあります。


実はそれまでのバージョンでも、Webサイトルール文書を作成することによって対応可能だったようです。
smaconne の利用マニュアルには「複数の IBM Domino サーバーを利用する場合のサーバー設定」として記載があります。※光安様、情報ありがとうございます。
また smaconne ではありませんが、Webサイトルール文書を使って設定する際の参考となりそうな情報はこちらにもありました。10.0.1 FP2 以前の Domino をご利用の場合は参考になるのではないでしょうか。

では FP2 ではどのようにして許可の設定を行うのでしょうか。

設定方法はこちらにありますが、次の3つの手順を踏みます。

  1. サーバー文書で CORS を有効にする
  2. CORS のルールを記述したファイル(ファイル名: cors-rules.json)を作成し、Domino\Data\Domino\CORS フォルダへ保存する
  3. HTTP タスクを再起動する

サーバー文書の設定は、Internet Protocols タブ - HTTP タブ - DSAPI のくくりにある 「DSAPI filter file name」に ncorsext を追加します。

手順の2番目で作成する json ファイルのサンプルはこちらにあります。

今回テスト環境用に作成した、CORS のルールを記述したファイル「cors-rules.json」の内容は次のとおりです。
{
  "version": "1.0",
  "rules": [
    {
      "resource": {
        "path": "/api/data/collections"
      },
      "allowOrigins": [ "http://localhost:3000" ],
      "allowMethods": [ "GET" ],
      "allowCredentials": true
    }
  ]
}


node.js アプリのオリジン「http://localhost:3000」から Domino の 「/api/data/collections」への GET 要求を許可しています。


CORS のルールが HTTP タスク起動時に正常にロードされると、コンソールに次のように表示されました。
2019/07/26 15:07:04   HTTP Server: DSAPI CORS Filter Loaded successfully

参考までに json ファイルに誤りがあった際にコンソールに表示されたメッセージがこちらです。
2019/07/25 20:45:11.36 cors::InitConfig> Unable to parse configuration C:\IBM\Domino\data\domino\cors\cors-rules.json
2019/07/25 20:45:11   HTTP Server: Failed to load DSAPI module ncorsext
※上で紹介しました「json ファイルのサンプル」のページに記載されている例には、不要なカンマが含まれており、そのままコピーするとロードに失敗します...

以上の設定を行い、http://localhost:3000 へアクセスしたところ、オリジンが異なる Domino (http://localhost)のデータにアクセスすることができ、取得したデータが表示されました。
左側が取得できた json を表示したもの。右側は取得した際のデバッグ情報

CORS に対応できたことが確認できました。


試しに CORS のルールで、allowOrigins のポート番号を変えたり、allowMethods を POST に変えたりしてみたところ、一致するルールが無いため 401 Unauthorized となりました。

なお allowMethods に "OPTIONS" も追加したほうが良いのかもしれませんが、今回の環境ではあってもなくても変化が見られませんでした。

2019年7月21日日曜日

おれおれ APM で Domino の統計情報をグラフ化

以前のエントリでも紹介しましたが、V10 では統計情報を New Relic というSaaSの監視サービスと連携する新機能があります。

実は、New Relic だけでなく、他の監視サービスとも連携できそうだということはオンラインヘルプにも書かれていたのでなんとなく知識としてはありましたが、これまで試したことはありませんでした。

ところで "New Relic" をグーグル先生に聞くと「APM」という単語が頻繁にヒットします。
この APM とは「アプリケーション性能管理」を指す一般的な用語らしいのですが、世の中には APM を謳うサービスが New Relic 以外にも多く存在することを最近知りました。

参考までに、グーグル先生が教えてくれた APM 関連製品/サービスを羅列してみます。
Datadog, Nagios, Stackify, Sensu, Pingdom, LogicMonitor, Splunk, Grafana, Scout, SteelCentral, Applications Manager, AppDynamics, dynaTrace, CA APM, JENNIFER, Hosted Graphite
※これらが Domino と連携できるかどうかは未確認です

この中のいくつかを試してみたくなったのですが、New Relic のようにグラフ化されない統計情報があるのだろうし、その場合にどうすればグラフ化できるのかを調べる手間よりも、自前でグラフ化できるのならそっちの手間のほうが楽しそうだと考えました。


さしずめ「おれおれAPM」です。


まずは、New Relic 等への統計の連携機能が REST API を使って POST していることを確認するため、POST 先として Domino の nsf ファイルを試してみることにしました。

まずは、POST を受けるために HTTP タスクを起動します。

サーバーコンソールから次のコマンドを投入しました。
load http


ドミノディレクトリのサーバー文書で、Domino Access Service (DAS) を有効にします。
また、統計情報を保存する NSF ファイル「statpub.nsf」とビューにおいても DAS を有効にしておきます。

※DASを有効にするための設定については「IBM Domino REST API 利用ガイド」が詳しいです


私の環境では、現在の統計の連携先が New Relic なので、それをやめるために notes.ini から NEWRELIC_LICENSE_KEY= で始まる行を削除しています。

そして Domino の NSF ファイルへ統計情報を POST するよう、notes.ini へ次のように設定しました。
STATPUB_ENABLE=1
STATPUB_URI=http://localhost/statpub.nsf/api/data/documents
STATPUB_DATA_HEAD={"host":"xsp13/v10","stattime":$Timestamp$000,
STATPUB_DATA_TAIL=}
STATPUB_METRIC_FORMAT="metric.$Name$":$Value$
STATPUB_DELTA_METRIC_FORMAT="metric.delta.$Name$":$Value$
STATPUB_HEADERS=Content-Type: application/json$Newline$Accept: application/json$Newline$StatTimeStamp: $Timestamp$Newline$

New Relic 以外へ連携する場合は STATPUB_ で始まるパラメータを必要に応じていくつか指定します。

1行目の STATPUB_ENABLE= は統計の公開を有効にする場合は 1 を指定します。無効は 0 です。

2行目の URI は、REST API を使用して NSF に文書を作成する場合に指定するものと同じです。

3,4行目は、統計情報を JSON 形式とするために最初と最後に{}を付加したり、後から使うであろうサーバー名などを追加しています。stattime にはタイムスタンプとしてUNIXエポックの日時を付加できるのですが、ミリ秒の部分が欠落しており、後述の Chart.js で使う場合に都合が悪いので、3桁のゼロを追加しています。

5,6行目は統計情報のフォーマットを定義します。統計情報にある Update.DefferdList といった名前の前後にダブルコーテーションを加え、名前と値の区切り文字としてコロンを指定しています。なお、6行目の delta で始まる統計情報には、前回の値との差を示す値がセットされるようです。

7行目には POST する際にヘッダとして指定するべき Content-Type といったパラメータなどを定義しています。複数のパラメータを改行で区切るために $Newline$ を挟んでいます。
また、このままでは NSFファイル内に作成される文書の作成者は Anonymous となりますが、もし、作成者を特定のアカウントにしたい場合は「Authorization: Basic <アカウント:パスワード>」を追加すると Basic 認証できます(<>には BASE64でエンコードした文字列を設定します)。


さて、これを設定して数分待っていると statpub.nsf に文書が登録されていました。


こうしてこの連携機能が REST API を使って POST していることが確認できました。


ところで、APM はリアルタイムにグラフ化できることがメリットのひとつだと思うのですが、NSFファイルにあるデータをリアルタイムにグラフ化するには、NSFにデータが作成された瞬間に値をグラフへ反映する仕組みが必要です。また、そもそもグラフをどうやって表現するかということにも関係します。

実は現在参加しているノーツコンソーシアムの「アプリ開発研究会」で、リアルタイムにグラフ化するほうのネタを提供してしまったため、このエントリでは「リアルタイム」をちょっと犠牲にして実現する方法をご紹介します。

POST 先のデータベース「statpub.nsf」に、次のようなビューを追加しています。
1列目)列の値:statpub、最新のデータを最も上に表示するため降順でソートします
2列目)列の値:metric.Update.DeferredList
3列目)列の値:metric.Update2.DeferredList

グラフには2つの統計値を表示したいので、2列目と3列目で値を表示しています。
今回は「Updateタスクのキューに入っている要求の数」を見ることを想定して 「metric.Update.DefferdList」を指定しました。なお、私の環境は2つのUpdateタスクを起動しているので Update2 の統計情報が存在します


さらにページ「index」を追加しています。データベースをWebブラウザで開く際にこのページが開くようデータベースのプロパティを変更しています。

ページには次のコードを記述してあり、このコード全体をパススルーHTMLとしています。
</form>
<canvas id="myChart"></canvas>
<script type="text/javascript">
    var ctx = document.getElementById('myChart').getContext('2d');
    var chart = new Chart(ctx, {
        type: 'line',
        data: {
            datasets: [{
                data: [],
                label: 'Update.DeferredList'
            },{
                data: [],
                label: 'Update2.DeferredList'
            }]
        },
        options: {
            elements: {
                line: {
                    tension: 0
                }
            },
            scales: {
                xAxes: [{
                    type: 'realtime'
                }]
            },
            plugins: {
                streaming: {
                    duration: 600000, //保持する時間幅=画面の横幅(ミリ秒)
                    refresh: 15000, //次にデータを読み込むまでのインターバル(ミリ秒)
                    delay: 0, //最新のデータを表示する間隔(tension 0 の場合、関係なし)
                    frameRate: 30, //数字を大きくするとスクロールがスムーズになる
                    pause: false,

                    onRefresh: function(chart) {
                        var json = '';
                        var xhr = new XMLHttpRequest();
                        xhr.onreadystatechange = function() {
                            if (xhr.readyState === 4) {
                                if (xhr.status === 200) {
                                    json = JSON.parse(xhr.responseText);
                                    chart.data.datasets[0].data.push({
                                        x: json[0]["stattime"],
                                        y: json[0]["metric.Update.DeferredList"]
                                    });
                                    chart.data.datasets[1].data.push({
                                        x: json[0]["stattime"],
                                        y: json[0]["metric.Update2.DeferredList"]
                                    });
                                } else {
                                }
                            }
                        }
                        xhr.open( 'GET', 'http://localhost/statpub.nsf/api/data/collections/name/Update.DeferredList?count=1&systemcolumns=0x80a');
                        xhr.send(null);
                    }
                }
            }
        }
    })
</script>
<form>

また、このページの HTML Head Content には以下のように指定しています。
"<meta charset=\"UTF-8\">
<title>リアルタイム統計情報</title>
<script src=\"https://cdn.jsdelivr.net/npm/moment@2.24.0/min/moment.min.js\"></script>
<script src=\"https://cdn.jsdelivr.net/npm/chart.js@2.8.0\"></script>
<script src=\"https://cdn.jsdelivr.net/npm/chartjs-plugin-streaming@1.8.0\"></script>
<style>* { margin: 0px; padding: 0px; }</style>"


このページに書いたコードを簡単に説明します。

統計情報の折れ線グラフを時系列に表示したかったので、今回は Chart.js に chartjs-plugin-streaming プラグインを組み合わせています。グラフは canvas タグの場所に最新の10分間(単位をミリ秒で指定するためコード上は600000)だけ表示する設定です。

ここでは、15秒間隔(単位をミリ秒で指定するためコード上は15000)でリフレッシュしていますが、そのタイミングで XMLHTTPRequest を使ってビューの1行目(最新の統計値)だけを取得します。取得できた JSON データをパースして、日時値と表示したい統計情報の値をグラフに反映します。

グラフを表示するにはブラウザから http://localhost/statpub.nsf へアクセスします。

こうして表示されたグラフがこちらです。※実際はもたもたと右から左へ動きます。


ひとまず「おれおれAPM」ができました。

2019年6月29日土曜日

NotesHTTPRequest の get メソッドで Type mismatch(後編)

前のエントリでは NotesHTTPRequest の get メソッドを実行し、その戻り値を NotesJSONNavigator クラスへ直接セットしたとき Type mismatch とメッセージが表示されることがあり、そうなる場合の可能性として
  • Content-Type が application/json ではなく text/plain であること
を指摘しました。

その点について、今回は node.js を使って検証してみようと思います。

node.js とは JavaScript の実行環境です。非常に簡単に web アプリを実装できます(レベル感はそれぞれと思いますが)。
私は自宅の Windows 10 に node.js をインストールしており、近い将来 AppDev pack について学習する準備を整えようとしていて、一冊の node.js の入門書を終えたところです。

ですので node.js とその周辺技術についてはほぼほぼ初心者であることを予めお伝えします。また express のインストールなど詳細は省きます。ボロがでますので。

今回、検証用に用意した node.js  側のアプリ app.js のコードは次のとおりです。
const app = require('express')();
const http = require('http').Server(app);

http.listen(3000, () => console.log('Server started'))

app.get('/', function(req, res) {
    var content = [{
        "short": "下丸子図書館 ",
        "address": "東京都大田区下丸子二丁目18番11号",
    }];

    // パターン1:JSONを文字列に変換
    res.json(content);

    // パターン2:Content-Type が application/json
    //res.writeHead(200, {'Content-Type': 'application/json; charset=utf-8'});
    //res.write(JSON.stringify(content));
    //res.end()

    // パターン3:Content-Type が text/plain
    //res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
    //res.write(JSON.stringify(content));
    //res.end()
})

自宅のパソコンに Notes クライアントと node.js を同居させています。
app.js の実行中に、localhost のポート 3000 番にアクセスすると JSON (あるいはそれっぽいもの)を返します。

JSON のデータは変数 content にある JSON 文字列を元に3パターン用意しています。パターンの切替は、コードの行頭にある // の削除、あるいは行頭へ // を追加しています。
  1. JSONを文字列に変換して返す
  2. JSONを文字列に変換して Content-Type に application/json を指定して返す
  3. JSONを文字列に変換して Content-Type に text/plain を指定して返す


そしてNotesクライアントで実行するエージェントは、前回使用したコードの "url =" の部分を "http://localhost:3000" に書き換えたものになります。


さて結論から言うと、この3つのパターンのうち3番目だけ Type mismatch が発生しました。

パターン1 には Content-Type を指定していませんが、Notes クライアントでレスポンスヘッダーを見ると application/json になっていました。

つまり、パターン1とパターン2のように、レスポンスヘッダーの Content-Type が application/json の場合、Type mismatch とならなかったことがわかりました。

なお文字列中の改行については、パターン2とパターン3で JSON.stringify(content, null, 2) として改行を含んだ文字列にしましたがが、結果に変化はみられませんでした。

もし、NotesHTTPRequest の get メソッドで Type mismatch となった場合、レスポンスヘッダーにある Content-Type を確認してみてはいかがでしょうか。

NotesHTTPRequest の get メソッドで Type mismatch(前編)

10.0.1 の FP2 で拡張された NotesHTTPRequest のプロパティと NotesJSONNavigator を試しました。

概要は、次の Technical Blog Post にあります。
Limitations of NotesHTTPRequest and NotesJSONNavigator with future considerations

FP2 では NotesHTTPRequest に新たに2つのプロパティ(PreferUTF8 と PreferJSONNavigator)を含むのだとか。

ありがたいことに、私が使用する Community 版にも FP2 が提供されていますので適用してみたところ、Designer でコードをタイピングしたときに追加されたプロパティも表示されることを確認できました。


これらのプロパティについて2019年6月末時点では Knowledecenter を検索してもヒットしませんし、上の画像のように説明もほぼありません。 詳しいことは分かりませんが、さっそく試してみることにします。

先の Technical Blog Post には、FP2より前とFP2のコードが紹介されており、 NotesHTTPRequest の戻り値から直接 NotesJSONNavigator クラスのインスタンスを作るには、事前に PreferJSONNavigator プロパティに 1 をセットしておく、と読めます。

そこで、次の鬼わかの記事でも利用している郵便番号検索サイトで試してみました。

連載 Domino V10 アプリ開発 #鬼わか 解説
第 6 回「NotesからREST APIを呼んで郵便番号検索をしてみよう #鬼わか 解説」

試したエージェントのコードは次のとおりです。
Option Public
Option Declare

Dim ss As NotesSession
Sub Initialize
 Dim jsonNav As NotesJSONNavigator
 Dim url$
 Set ss = New NotesSession
 url = "http://zipcloud.ibsnet.co.jp/api/search?zipcode=8480001"
 Set jsonNav = getJSON(url)
End Sub
Function getJSON(url$) As NotesJSONNavigator
 Dim http As NotesHTTPRequest
 Set http = ss.CreateHTTPRequest()

 http.Preferjsonnavigator = True
 Set getJSON = http.Get(url)
End Function

エージェントを実行すると、下から2行目で Type mismatch と出ます。


上のデバッグ・ウィンドウをよく見ると、レスポンスコードが 200 となっているので、検索結果は戻ってきているようです。ただし NotesJSONNavigator のインスタンスが作れていません。

ひょっとして body が JSON 形式じゃないのかも?

と考え、 notes.ini へ次の1行を追加して、console.log に記録されるデバッグ情報をみてみることにしました。

DEBUG_NOTESHTTPREQUEST=1 

すると、気になる点が2つありました。まず、レスポンスヘッダーの Content-Type が application/json ではなく text/plain でした。

LSXBE HTTP debug information type: HTTP Response header:
Content-Type: text/plain;charset=utf-8

もうひとつは data に改行が含まれていることです。

LSXBE HTTP debug information type: HTTP Response header:  
 Content-Type: text/plain;charset=utf-8
 
LSXBE HTTP debug information type: HTTP Response data:  
 "message": null,
 "results": [
  {
   "address1": "o¢ET│Ct£i",
   "address2": "o-eoccUciOce",
   "address3": "Oiuμ│oOnUto-UciμRi",
   "kana1": "´¢-´¢A´\×´¢-´\O",
   "kana2": "´¢-´\A´\y´¢-",
   "kana3": "´\E´\a´\E´\e´\C´\u´¢≪´¢│´¢-´\×´¢!´¢│´\e´\×´¢-",

ひょっとするとこれらが type mismatch の原因なのかも。


そこで、郵便番号検索とは別のサービス、カーリルさんの図書館APIを試してみることに。
上のコードの "url = " で渡す値を次のように書き換えます。

"http://api.calil.jp/library?appkey=[あなたのアプリキー]&city=大田区&format=json&callback="
※実際のコードへ設定する場合、アプリキーと、"大田区"の文字列はURLエンコードが必要です

実行してみたところ、エラーにならず NotesJSONNavigator クラスのインスタンスが作成できました。


このときのデバッグ情報は、レスポンスヘッダーの Content-Type は application/json であることに加え、data は 2つにわかれてはいるものの改行は含まれていないように見えました。

LSXBE HTTP debug information type: HTTP Response header:  
 Content-Type: application/json; charset=utf-8

LSXBE HTTP debug information type: HTTP Response data:  
[{"category": "MEDIUM", "city": "Onoto-Oi-", "short": "ocioccO!EOo│μocUn- ", "tel": "03-3759-2454", "pref": "μO-o--Ua¢", "faid": null, "geocode": "139.6880403,35.5658561", "systemid": "Tokyo_Ota", "address": "μO-o--Ua¢Onoto-Oi-ocioccO!Eo-iocuto≪18to¬11OAA
LSXBE HTTP debug information type: HTTP Response data:  
to¬14OAA Onoμu≪TncOEeμu¢T-!OaoOa-LuzOnoμu≪Oaa", "libid": "109666", "libkey": "OaNμu-o-oOo│μocUn-", "post": "143-0016", "url_pc": "http://www.lib.city.ota.tokyo.jp/", "systemname": "μO-o--Ua¢Onoto-Oi-", "isil": "JP-1000983", "formal": "Onoto-Oi-t-iOaNμu-o-

これだけでは断定できませんが、可能性のひとつかもしれません。

次のエントリでは、 node.js を使って検証してみようと思います。