2019年12月7日土曜日

HCL Factory Tour 4 in Tokyo に参加しました

12月4日に東京コンベンションホールで開催された HCL Factory Tour 4 in Tokyo に参加しました。

海外のイベントに参加したことの無い私にとって、こんなに興奮した Notes/Domino 関連のイベントはこれまでなかったかもしれません。

ありがたいことに HCL Master には最前列がリザーブされてました。私は2列目の中央に陣取りました。平坦な会場ではまさに特等席でした。
この距離感。最前列は HCL Master 加藤氏のみ。
この席から私の iPhone で撮影したスライドとコメントを残しておこうと思います。
※以下Notes/Domino ばかりでごめんなさい

冒頭に登壇した Richard Jefts 氏は日本市場の重要性について語りました。
Richard Jefts 氏
日本に特化した戦略と投資について。
HCL Digital Solutions にとっての日本
ノーツコンソーシアム、のの会といったコミュニティやパートナーとのエコシステム。のの会関係者としては会場や設備といった面でこれからに期待が持てました。
戦略的パートナーとコミュニティとのエコシステム
IBM Cloudインフラにあるメールを移行するためのツールの提供と、移行先となるパートナーについて。ISW の右下に小さく日本の KTrick 社の文字も。
現行のクラウドメールの移行先となるパートナーと移行ツール

3製品についてロードマップ的な発表がありました。(次の3つのスライドの製品バージョンは今月でてくるものではなく、その1ステップ先のものです)
HCL Domino V12

HCL Connections V7

HCL Sametime V12
次は Json Gary 氏です。私がこのイベントで最も楽しみにしていたセッションです。彼は Domino の進化の方向性について熱心に語りました。

現在の Domino V10 も Docker 対応を謳っていますが、OS込みのイメージでなければなりません。しかしながら将来の Domino はずっとポータブルになりそうです。
クラウドネイティブプラットフォームとしての Domino へ進化

そしてコンテナ化された Domino や DX, Sametime, Connections が Kubernates 上で動作しているイメージも提示されました。ここから数枚は勉強不足によりついていけず。
k8s で動作する Domino や Connections
イベント駆動アーキテクチャと CloudEvents
標準イベントモデルに合わせ、統合を加速
会議室の状況をモニタリングするIoT(センサー)の情報で Dominoの予約データを更新し、更新イベントを受けて Microsoft Teams へ反映する、というイメージが示されました。
IoTとDomino、Microsoft Teamsとの統合のイメージ
次の画面は、先の Domino V12 のスライドにあった Designer Reimagined に相当するものと思います。LotusScript が VSCode に表示されています。
VisualStudio Code 上の LotusScript
次のスライドにある Translation Services とは LotusScript から JavaScript への変換を意味しているのだそうです。この点については嫌な予感がしなくもない。
Project Keep の概要

続いて Andrew Manby 氏から V11 の紹介です。
その前に、ローコード/ノーコードの開発ツール「HCL LEAP」がDomino上で動くよ!とアナウンスされてきましたが、既存の「HCL LEAP」と区別され、Dominoで動くほうは「Domino Volt」となることがわかりました。Volt の図柄は稲妻でイメージしやすいですね。
Domino版の名称は Domino Volt
V11 で注目すべき3つに焦点を絞って紹介しました。

  • Domino Volt
  • Domino Nomad
  • ActiveDirectory同期

Volt, Nomad, AD同期

新たに Low-code 基盤を追加

Domino Volt の eGA は 来年1H

Nomad は iPad のほか iPhone, Android, ChromeOS にも対応

Event Publisher は「文書の追加」といった Domino 上のイベントを公開する

ユーザーはAD上のユーザーのメールアドレスを知らなくても探せるようになる

Sametime はサードパーティーのWebミーティングとの統合をサポート

Notes V11 と Verse オンプレミス (VOP) 1.0.9

Domino V11 の拡張

前回の投稿で触れました DAOS tier 2 ストレージについて、イメージが更新されていましたのでひとこと。これまでの発表(資料のP27)を下(このイベント)のイメージと比較すると、tier 2 のオブジェクトが複数のままに変わったことがわかります。どこかで検証してみたいポイントです。
DAOS tier 2 ストレージの「新しい」イメージ
DQL では全文索引とワイルドカードを使用可能に
Domino 製品戦略
新たなライセンスモデル

ここまでが昼食前。昼食はお弁当が用意されており、おいしくいただきました。※写真撮り忘れた...

そして午後です。
再び登場の Andrew Manby 氏。いよいよ軽量クライアントが出てくるかと思って勝手にわくわくしている私。
シンクライアント戦略
ブラウザ内でNotesが動く、というイメージが示されました。WebAssembly で実現する軽量クライアントはインストール不要になります。
動くデモは見られず...残念!!
Verse のロードマップ

Verse 1.0.8, 1.0.9
 Verse 1.0.10 のスライドに Progressive Web App という言葉が登場しました。これはどのブラウザでも動作するウェブアプリです。パフォーマンスが気になります。
Verse 1.0.10 と PWA

SAML で TLS 1.2 をサポート


このあと、DX と Sametime と Connections についてそれぞれのトップによるセッション、(開発者ではない)Tim Clark 氏による Domino Volt を使ったアプリ開発のデモなどありました。Volt で作成したアプリは Domino Designer で開きますが、既存アプリをモダナイズするといった用途では使えません。

ここでバッサリ省いた内容については セッションの動画が公開されましたので、そちらでご確認いただけるかもしれません。

登壇者はグローバルの製品のトップばかりでした。最高の製品・サービスであり続けるために取り組んでいる事への情熱や自信といったものが伝わってきました。

Notes/Domino V11 のダウンロードは今月(2019年12月)20日(日本時間ではないと思いますが)に利用可能になります。いよいよですね!

2019/12/11 追記
日本語のセッションの資料英語のセッションの資料が公開されました。

2019年12月1日日曜日

DAOS tier 2 ストレージ

リッチテキストフィールドに添付したファイルは通常、データベース( .nsf ファイル)内に保存されますが、添付されたファイルの代わりに「チケット」をデータベース内に保存し、ファイルの実体をデータベースの外(レポジトリ)へ移動して .nlo という拡張子の「オブジェクト」として保持する DAOS (Domino Attachment and Object Service)という機能があります。

リッチテキストへの添付が便利すぎるため、ユーザーはサイズの大きいファイルを文書へ添付しがちですが、DAOS (私はダオスと呼称しています)がすばらしいのは、文書にファイル添付してサーバー上のデータベースへ保存するとか、他のDominoへ複製するなどして文書を転送するときに、転送先のレポジトリに添付ファイルと一致する DAOS のオブジェクトがあれば、添付ファイルの実体を転送せず、添付ファイルをチケットに自動で置き換えて nsf へ保存します。レポジトリに無い場合は、実体をレポジトリに移動します。nsf のサイズとサーバーへ転送するサイズを節約してくれます。

転送と書きましたが言い方を変えると、メールデータベースで DAOS を有効にした場合、サイズが 30MB の動画ファイルを添付したメールを同じホームサーバーを使う1000人へ送ったとしても、サーバー上の1001人のメールデータベース内のメール文書にはチケットがあるだけで、添付ファイルの実体はレポジトリに移動した1つのオブジェクトだけです。DAOSが無効の場合と比較すると、サーバー内のストレージは 30MB x 1000 = 約 30GB が節約されることになります。なお DAOS はメール以外のデータベースでも設定することができます。


さて、2019年12月にリリースされる Domino の次期バージョン V11 には、DAOS のストレージを2層にして、レポジトリ(tier 1)にあるオブジェクトのうち、一定期間参照されないものを S3 互換ストレージ(tier 2)へ移動する、という拡張があります。

直近のイベントで目にした、この拡張のイメージ図(「V11新機能と今後のロードマップ」27ページ)では、複数の Domino サーバーのレポジトリ(tier 1)にあるオブジェクトが、S3 互換ストレージ (tier 2) へ移動するのですが、「実体の同じ」オブジェクトがすでに tier 2 に存在すれば、新たに tier 2 へ移動することなしに tier 1 のオブジェクトを削除する、という風に見えます。※そんな説明は資料にはありません

Domino内の複数のDBから参照される同一の添付ファイルが、Domino内の tier 1 で1つのオブジェクトで管理され、さらに複数のサーバーにある同一の添付ファイルが tier 2 で1つのオブジェクトで管理されるようなのです。

ところで、レポジトリにあるオブジェクトは拡張子が nlo のファイルですが、デフォルトでは元サーバーの鍵で暗号化されます。暗号化された nlo は、他のDominoサーバーのレポジトリに作成されたオブジェクトを持ってきても使えないようになっています。

暗号化された nlo をそのまま tier 2 へ移動したのでは、他の Domino からオブジェクトを参照できても内容を読むことができないのではないでしょうか。

tier 1 に作成されるオブジェクトで暗号化を無効にすることは現行バージョンでも可能です。notes.ini に DAOS_ENCRYPT_NLO=0 を追加しておきます。

Domino V11 ベータ2 の What's New には、tier 2 を設定するにあたり暗号化を無効にしなければならないとは書いていません。複数の Domino で tier 2 を共有する場合の設定にも触れていますが、オブジェクトの暗号化については触れていませんでした。
また、暗号化されたオブジェクトを tier 2 へ移動する際、自動で復号化される、といった細かい話にも触れていません。

もし、現状 DAOS を複数の Domino で使っていて暗号化をデフォルトのまま有効にしている場合、同一ファイルから生成された nlo でも tier 2 上では Domino の数の nlo が作成されるかもしれないのです。これでは少々つまらないですよね。

そんなことを考えながら、ノーツコンソーシアム「ザ・ノーツ研究会」11月度の活動の中で検証してみようと取り組んでみましたが、敢え無く時間切れとなってしまいました...うーん、気になる!

2019年10月31日木曜日

GPS ロガー的なアプリを作ってみた

前回のエントリーで HCL Nomad で NotesGPS クラスが動いたことを書きましたが、今回はその続きで、GPSの位置情報を継続的に保存する仕組みを作ってみたお話です。

LotusScript には NotesTimer という私があまり使ったことの無いクラスが存在します。NotesTimer クラスを使うと、指定した間隔(秒単位)ごとにイベントを発生させることができます。

定期的に発生するイベントでGPSからの位置情報を保存すれば、GPSロガー的なものになりそうです。


まずは、フォームを1つ作成して以下のコードをフォームへ追加しました。

(Globals)の(Declarations)
Dim elapsedTimer As NotesTimer
Dim gps As NotesGPS
Dim position As NotesGPSPosition
Dim ss As NotesSession
%INCLUDE "lsconst.lss"

(Globals)の Initialize
Sub Initialize
 Set  ss = New NotesSession
End Sub

QyeryOpen
Sub Queryopen(Source As NotesUIDocument, Mode As Integer, Isnewdoc As Variant, Continue As Variant)
 On Error 4508 Goto ERROR4508
 
 Set gps = ss.CreateGPS
 gps.TimeoutSec = 9
 gps.HighAccuracy = False
 If Not gps.RequestAccess Then
  Messagebox "False",,"Request access"
  Exit Sub
 End If
 Set position = gps.GetCurrentPosition
 Call gpslog( "Start" )
 Exit Sub
 
ERROR4508:
 Messagebox "NotesGPS class のインスタンスを取得できません。" & Chr(10) & "Err: 4508"
 Continue = False
 Exit Sub
End Sub

PostOpen
Sub Postopen(Source As NotesUIDocument)
 Set elapsedTimer = New NotesTimer(60, "")
 On Event Alarm From elapsedTimer Call elapsedTimerHandler
End Sub

QueryClose
Sub Queryclose(Source As NotesUIDocument, Continue As Variant)
 elapsedTimer.Enabled = False
 Call gpslog( "End" )
End Sub

さらに次の2つのコードを追加しました。

Sub elapsedTimerHandler(Source As NotesTimer)
 Call gpslog( "" )
End Sub
Sub gpslog( msg As String )
 Dim ndt As NotesDateTime
 Dim doc As NotesDocument
 Dim coodinates As NotesGPSCoordinates
 
 position.Update
 Set ndt = position.TimeStamp
 Set coodinates = position.Coordinates
 
 Set doc = New NotesDocument( ss.CurrentDatabase )
 
 doc.Form = "Main"
 doc.Message = msg
 doc.gps.TimeoutSec = gps.TimeoutSec
 doc.gpsposition.TimeStamp = ndt.LSLocalTime
 
 doc.Latitude = coodinates.Latitude
 doc.Longitude = coodinates.Longitude
 doc.Altitude = coodinates.Altitude
 doc.Heading = coodinates.Heading
 doc.Speed = coodinates.Speed
 doc.Accuracy = coodinates.Accuracy
 doc.AltitudeAccuracy = coodinates.AltitudeAccuracy
 
 doc.Save True, False
 
 Set doc = Nothing
 Set ndt = Nothing
End Sub


このフォームを開くとき、NotesGPS のオブジェクトを作り、イベント発生の間隔を60秒にセットします。イベント発生ごとにコールするスクリプトを「elapsedTimerHandler」としました。

このフォームを閉じるとき、NotesTimer を非アクティブの状態にします。これでイベントが発生しなくなります。


さて NotesTimer のヘルプには次のような記述があります。
NotesTimer はエージェントではなく Lotus Notes UI オブジェクトで使用することを意図されています。
なんだか嫌な予感が...

HCL Nomad でこのフォームを前面に開いてる間は 約60秒おきに位置情報が保存されました。

フォームを開いて記録している間、HCL Nomad 上には、3つのタブ(ホーム、GPS Logger ビュー、フォーム)がありましたが、GPS Logger ビューのタブをタップしてフォームのタブが他のタブのうしろにある状態でも、位置情報は記録され続けました。※下図はフォームのタブが前面に表示されてます

しかしながら、画面の操作をしばらくやめて画面が暗転した状態では、位置情報が記録されませんでした。また HCL Nomad から別のアプリに切り替えている間も位置情報は記録されませんでした。

それから、 iPhone などのスマホでは他のアプリからの通知が表示されることがありますが、通知が表示されている間は位置情報は記録されていないような印象です。

位置情報が記録されなかった場合も、フォームを開いている間はタイマーが有効のようで、指定した間隔どおりにはなりませんでしたが遅れて記録され続けました。

こんなことがあったので、フォームを開いた後は、画面が暗くならないように画面をサワサワと触り続けたり、通知が表示されればすぐさま画面の外へスワイプするといったつまらない操作を継続的に行っていました。

そんなこんなで散歩ついでに取得した位置情報から KML ファイルを作り、Google Map へ読み込んでみると、次のように表示できました。→KMLファイルの作り方


GPS ロガーとしては、なかなか使いづらいものが出来上がりました。

2019年10月29日火曜日

Nomad で GPS を使う

つい数日前に Notes/Domino V11 Beta 2 がダウンロード可能になりました。

このベータ版で、私が楽しみにしていた機能のひとつがようやく実装されたのです!

実は V11 Beta 1 の Domino Designer でも LotusScript に新たに追加された3つのクラス、
NotesGPS
NotesGPSPosition
NotesGPSCoodinates
が見えてはいるのですが、NotesSession クラスに CreateGPS メソッドが実装されていないために実質使えなかったのです...

V11 Beta 2 の Domino Designer では、これらが動くことを確認しました。

次のコードがこれらのクラスの動作を確認できたものです。

Dim ss As New NotesSession
Dim gps As NotesGPS
Dim gpscoodinates As NotesGPSCoordinates
Dim gpsposition As NotesGPSPosition
Dim ndt As NotesDateTime
Dim msg$

Set gps = ss.CreateGPS
gps.TimeoutSec = 9
gps.HighAccuracy = False
If Not gps.RequestAccess Then
 MessageBox "False",,"Request access"
 Exit sub
End If

Set gpsposition = gps.GetCurrentPosition
Set ndt = gpsposition.TimeStamp
Set gpscoodinates = gpsposition.Coordinates

msg = _
"タイムスタンプ: " & ndt.LSLocalTime & Chr(10) &_
"緯度(Latitude): " & gpscoodinates.Latitude & Chr(10) &_
"経度(Longitude): " & gpscoodinates.Longitude & Chr(10) &_
"高度(Altitude): " & gpscoodinates.Altitude & Chr(10) &_
"緯度経度の誤差(Accuracy): " & gpscoodinates.Accuracy & Chr(10) &_
"高度の誤差(AltitudeAccuracy): " & gpscoodinates.AltitudeAccuracy & Chr(10) &_
"方角(Heading): " & gpscoodinates.Heading & Chr(10) &_
"速度(Speed): " & gpscoodinates.Speed

MessageBox msg

このコードを私の iPad 上の HCL Nomad 1.0.5 (TestFlight版)で実行すると、次のように表示されました。


ここで取得した緯度と経度の値で現在地がわかります。(自宅なので隠しましたが..)

GPS レシーバの無い端末(=私のパソコン)で実行すると、CreateGPS メソッドでコード 4508 Method is not available となりました。


さて、私がこの GPS の使い道として考えているのは、現在地に最も近い現場の情報を自動で入力/表示することです。

現在地に関係している情報とは、国、地域、住所、現場の名称の他、顧客名、プロジェクト名やこれらのコードなど意外に多いように思います。

また、iPad を持ちながら、HCL Nomad の入力画面をソフトウェアキーボードでタイプすることはかなりしんどい作業になります。リストから選択するにしても、リストが1000件にもなれば小さい画面の中で縦スクロールして見つけ出すことさえ困難なように思います。

そんな時、例えば「現場マスター」へ緯度経度の値を追加しておくことで、現在地から最も近い現場の情報を候補として表示できるようになりそうです。
たったこれだけでも使い勝手がよくなるのではないでしょうか。


これを実現するには、マスター上の緯度経度と現在地の緯度経度から「最も近い」場所を調べる必要があると考え、距離を求めるコードを書いてみました(以下)。戻り値の単位はメートルです
(lat は緯度、lng は経度、lat1 と lng1 が一方の地点、lat2 と lng2 が他方)

Function distance( lat1 As Double, lng1 As Double, lat2 As Double, lng2 As Double ) As Double
 Dim radLat1 As Double, radLng1 As Double
 Dim radLat2 As Double, radLng2 As Double
 Dim aveLat As Double, aveLng As Double
 Dim c As Double
 Const r = 6378137.0 '赤道半径
 
 '円弧の長さを扱うため、角度(緯度経度)をラジアンへ変換
 c = 180 / Pi
 radLat1 = lat1 / c
 radLng1 = lng1 / c
 radLat2 = lat2 / c
 radLng2 = lng2 / c
 
 aveLat = ( radLat1 - radLat2 ) / 2
 aveLng = ( radLng1 - radLng2 ) / 2
 
 distance = r * 2 * Asin( Sqr( Sin( aveLat ) ^ 2 + Cos( radLat1 ) * Cos( radLat2 ) * Sin( aveLng ) ^ 2 ) )
End Function

例えば、マスターに登録した緯度と経度の値を列に表示するビューを用意しておけば、最も近い情報を入力の候補として表示する、といった感じのものは簡単に作れそうです。

2019年10月17日木曜日

Nomad でも OCR

昨日の会社帰りは、約2カ月ぶりに開催された「のの会」に参加しました。


今回、私が担当したのは「iPad で OCR を試した事」のお話です。

既にこのブログでも何度も登場している NotesHTTPRequest や NotesJSONNavigator等の V10 で追加された機能が iPad でも動くことを、実機のデモを通してご紹介しました。

私が所有する iPad の OS は iPadOS にしています。昨日は ThinkPad Bluetooth ワイヤレス・トラックポイント・キーボード を使って、トラックポイントのマウス操作で HCL Nomad とそのアプリがグリグリ動く様子をご覧いただきました。

iPad でデモする時は、設定から「アクセシビリティ - タッチ - AssistiveTouch」をオンにすると、マウスカーソル的なマークがわかりやすく表示されるのがいいですね。

お話した内容をスライドにしていますので、共有いたしました。何かの参考になれば幸いです。





2019年10月11日金曜日

iPhone? iPad? @Platform の拡張について

先日 TestFlight で公開されました HCL Nomad 1.0.5 では iPhone でも使用できるということで、早速ダウンロード。

このバージョンは @Platform 関数が拡張され、iPhone と iPad を区別することができる、と説明されています。

これを試してみました。

まずは、ビューのアクションボタンに次の式を設定しました。

@Prompt([Ok];"Formula";@Platform([Specific]))

これを実行してみると、iPad と iPhone のどちらも "iOS" とだけ表示されます。

「これはバグではないか?」と思い、フィードバックしたところ、すぐさまメールで返事がありました。

回答によれば、@Platform([Specific]) の戻り値は次のようなテキストリストだというのです。

PrimaryOSNamefor example, iOS
PrimaryOSVersionNamefor example, 12.4
iOS Model Typeeither iPad or iPhone
Apple Hardware Identifierfor example, iPad8.4

さらに次の式を iPhone で実行すれば "Platform is iPhone" と表示されると。

@If(@Platform([Specific])="iPhone";@Prompt([Ok]; "Formula"; "Platform is iPhone");@Prompt([Ok]; "Formula"; "Platform is NOT iPhone")) 


よくヘルプを読んでみるとバグでもなんでもなく「文字列リストが返されます」と書いてありました... orz


気をとりなおして、先のボタンの式を次のように書き換えてみました。

@Prompt([Ok];"Formula";@Implode(@Platform([Specific]);@NewLine))

そして表示されたプロンプトがこちらです。
iPhone
iPad
Windows 10
最後の Windows 10 はおまけですが、テキストリストの3番目にある iOS Model Type で iPhone か iPad を識別できることがわかりました。

おかげで私の所有する端末が古い機種ということがバレバレです(笑)

V11 では GPS にも対応するとのことですし、まだまだ Nomad 対応アプリの開発を楽しめそうです。

2019年10月7日月曜日

NDS2019 トラブルチケットアプリの部分的裏話

NDS2019 は5都市を巡り、どの会場も盛況で幕を閉じました。

今年5回目の開催となる NDS ですが、私は今回が初参加でした。

その NDS へ参加することが決まったころ...

日本の IBM Champion でアプリを作り NDS2019 の参加者へのお土産にしよう!

とか

どうせなら iPad でも使えるアプリにしよう

さらには

ついでに CollabSphere の Beauty Contest に応募しよう!

といった話が持ち上がり、各地の NDS 最後のビアバッシュ中に、ケートリック田付様よりご説明いただいた「トラブルチケット」を作ることになりました。



アプリの概要はスライドをご覧ください。

このアプリで私が主に関わったのは Slack への投稿部分です。

開発は Domino Designer のバージョン 10.0.1 FP2 で行っていました。
IBM Domino Mobile Apps(以下 DMA)は 1.0.3 です。

当初の構想では、文書に添付された写真を文書のタイトルとともに Slack へ表示しようと考えていました。ところが、LotusScript では配列の制限があり、ちょっとしたサイズのバイナリデータを送ることができないことと、Base64 エンコードしたデータを Slack 側がサポートしていないことの2点で、写真の送信をあきらめました。Google Drive 等 Slack 以外のクラウドストレージへ送信し、そのURLを Slack で使用するといった代替案もあったのですが、複雑にしたくなかったのです。

さらに悩ましかったのは、投稿の内容が英数字だけなら Notes クライアントも iPad の DMA もうまくいくのに、ダブルバイト文字が入ると iPad では文字化けする問題が発生したことでした。

iPad の DMA から投稿自体はできるものの、Slack に表示される全角文字が "BAD+82" のような文字列になって表示されてしまう現象に遭遇したのです。

エンコードを変えてみたりなんとかできないかと工夫しようと試みましたが、どうにもうまくいきません。

万策尽きて完全にあきらめていたところ、NDS 東京会場の開催前日になり DMA 1.0.4 (このバージョンから名称が HCL Nomad へと変わりました)が TestFlight でダウンロード可能となりました。

なんとこの文字化け、1.0.4 で見事に解消していたのです!

日本語の文字化けは DMA のバグが原因であり 1.0.4 で Fix されていたのでした。

そのような訳で、NDS会場のデモでは iPad 上の HCL Nomad からの投稿後直後に、画面上部からピロンと Slack 通知が日本語で表示されたのでした。めでたしめでたし。


なお、今回作成しましたアプリは設計を公開した状態で、XPAGES.JP のWebサイトからダウンロードできるようになるようですりました。

改行を含む文字列を Slack へ投稿したり、notes:// で始まるリンクを送るコツなど、NDSのビアバッシュでほろ酔いの状態では到底話せないネタを、私の稚拙で汚いコードが語ってくれると思います。

公開されましたらこのページを更新してリンクを掲載したいと思います。
こちらからダウンロード可能です。

2019年9月28日土曜日

LotusScript で携帯電話へSMSを送信

NDS2019 も折り返し地点を回り、残り2か所となりました。

ご来場されたお客様より伺ったお話から「やってみたい」と思うことがあって、早速試したことがありますので、ここに残しておこうと思います。

現状の最新版 Notes/Domino V10 には HTTP Request を送信できる新機能があります。

9.0.1 までは Windows の HTTP Request を呼び出したり Java でプログラムを組んだりしていましたが、これらは iOS 上のクライアント(HCL Nomad) では使えません。

また Notes クライアントは Windows のほか、Mac、iOS (現在は iPad のみ) で動くクライアントがありますが、OS依存の部分など多少考慮しなければならない場合があるものの、LotusScript のコードはどのクライアントでも動くようです。

V10 の新機能を使うことで、より Web サービスとの連携が容易になりました。


さて、やってみたいこととは、携帯電話への SMS (Short Message Service)の送信です。

Twilio (トゥイリオ)というサービスをご存知でしょうか。

詳しくは上のリンクをご覧いただくとして、Twilio は SMS や自動音声通話といった、電話番号を用いたサービスと連携するための API を提供しています。

電話をかけて、文章を日本語で読み上げたり、音声ファイルを再生したりといったようなことが可能なようです。

今回はこの API を使い、私のスマートフォンへ SMS を送信してみます。

SMS を送信する場合は、場合にもよるかもしれませんが料金が発生します。

Twilio の API は Free Trial が可能です。
Sign up を済ませて初回ログインすると、表示された Console Dashboard には 500円分の TRIAL BALANCE が付与されていました。

この Dashboard には、テストに使用する発信用の電話番号(TRIAL NUMBER = Twilio電話番号)を取得できるボタンがあったり、API を使う際に必要となる「アカウント SID」と「AUTH TOKEN」もあります。

なお、私が TRIAL NUMBER を取得したところ +1 で始まる米国の電話番号が取得できました。

次のコードは、サンプルとして作成したエージェントです。

Dim ss As NotesSession
Const accountSid = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Const authToken = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Const phoneFrom = "+11231231234" '発信用電話番号(Twilio電話番号)

Sub Initialize
 Set ss = New NotesSession
 sendSMS "LotusScript から初めての  SMS 送信。ドキドキ...", "+819012345678"
End Sub

Function base64Encode( textString As String ) As String
 Dim stream As NotesStream 
 Dim db As NotesDatabase 
 Dim doc As NotesDocument 
 Dim body As NotesMIMEEntity
 
 Set stream = ss.CreateStream 
 Call stream.WriteText ( textString ) 
 
 Set db = ss.CurrentDatabase
 Set doc = db.CreateDocument
 Set body  = doc.CreateMIMEEntity
 
 Call body.SetContentFromText ( stream, "", ENC_NONE )
 Call body.EncodeContent ( ENC_BASE64 )
 
 base64encode = Replace( Replace( body.ContentAsText, Chr( 13 ), ""), Chr( 10 ), "" )
 
 Call stream.Close
End Function

Sub sendSMS( message As String, phoneTo As String )
 Dim req As NotesHTTPRequest
 Dim url$, body$, res As Variant

 url = "https://api.twilio.com/2010-04-01/Accounts/" & accountSid & "/Messages.json"

 body = |From=| & urlEncode( phoneFrom ) & _
 |&To=| & urlEncode( phoneTo ) & _
 |&Body=| & urlEncode( message )

 Set req = ss.Createhttprequest()
 req.Preferstrings = True
 req.Setheaderfield "Content-Type", "application/x-www-form-urlencoded"
 req.Setheaderfield "Accept", "application/json"
 req.Setheaderfield "Authorization", "Basic " & base64Encode( accountSid & ":" & authToken )
 res = req.Post( url, body )
End Sub

Function urlEncode( textString ) As String
 Dim notesmacro As String, res As Variant

 notesmacro = |@URLEncode( "Domino";"| & textString & |" )|
 res = Evaluate( notesmacro )
 urlEncode = res( 0 )
End Function

以下、上のコードを補足します。

2~4行目
実行に必要なアカウントSID、AUTH TOKEN、発信者番号(Twilio電話番号)を定数(Const)で設定していますが、実行時は Console Dashboard のもので置換します。
※本番で使う場合、認証情報のハードコーディングはお勧めしません

4行目、8行目
このサービスのすばらしい点のひとつは、対応する国が日本だけではないことです。ここでは私のスマホの電話番号を指定したいので、最初のゼロを除いた番号の頭に日本の番号を示す +81 を付けました。たとえば電話番号が 090-1234-5678 の場合、発信先番号へは +819012345678 と指定します。

36行目
API の URL ですが、最後を .json とすると、レスポンス(47行目)が JSON 形式で返ります。このコードではレスポンスをほぼ無視していますが、SMSが送信されない理由を調べたい場合はコードの追加が必要です。※ res を messagebox で表示するだけでも、レスポンスにあるエラーコード等がなんとなく見えます

38行目、39行目
From(発信者番号)と To(受信者番号)と Body(メッセージ)は、URLエンコードが必要です。

46行目
API は Basic 認証をサポートしています。認証情報として
アカウントSID + ":" + AUTH TOKEN
の文字列を Base64 エンコードした値を "Authorization" ヘッダーへ追加しています。

そして、エージェントを実行して、実際に iPhone で受信したところが下図です。




今回はトライアルのアカウントを使用したからでしょうか、メッセージの冒頭には "Sent from your Twilio trial account - " と追加されてしまいました。まあ、仕方ありませんね...

SMS の料金などを調べずに3回 API をコールしてしまいましたが、Dashboard を覗いてみると、まだ 500円以内に収まっていました。
もう少し楽しめそうです..

もし「私も試してみたい!」と思った方は、Qiita に「[最新版]Twilioのサインアップ」という記事がありますので、ご参考まで。

2019年9月26日木曜日

V11 新機能 テーマのカラーを変えてみた

V11ベータ1 では、Notesクライアントのテーマの色を notes.ini パラメータを追加することで変更できる機能が追加されていました。

notes.ini へセットするパラメータは次のとおりです。
CUSTOM_THEME_COLOR=<R>,<G>,<B>
<R>,<G>,<B> には、それぞれ赤、緑、青を示す数値をセットします。

V11ベータ1のデフォルトのテーマは、新たに追加された "Notes 11 Theme" です。
私には少々青が強いように感じていますが、これが HCL 社のカラーなのでしょうか...

さて、昔懐かしのロータス・カラーへ変更したい場合、
CUSTOM_THEME_COLOR=255,211,2
あたりの値をセットします。

すると、メールを開いたときの見た目は次のようになります。


notes.ini パラメータであれば、デスクトップポリシーを使ってクライアントへ設定を配布することができます。

そこで今回ご紹介した notes.ini パラメータをデスクトップポリシーを使って配布するための設定手順をスライドにまとめました。
説明がほとんどありませんがご容赦くださいませ。





2019年9月10日火曜日

V11 の DQL はさらに進化!全文索引の検索も可能に!

Notes/Domino の次期バージョンであるV11のベータプログラムが始まりました。

https://hcljapan.co.jp/software/blog/hcl-notes-domino-v11-beta-started

私もさっそくWindows版のNotesとDominoをダウンロードして、試しているところです。

このベータで私が最も注目している機能のひとつに「DQLの拡張」があります。

DQL は V10 の新機能として追加された、データベースの文書へアクセスするための新たな枠組みです。V10 では NSFサーチ(文書のサマリーフィールド)とビューを検索できるのみで、サマリーフィールドでもないしビューにも表示できない「リッチテキストフィールド」の内容は、検索できませんでした。

V11 ではこれが改善され、全文索引を検索するためのクエリーを書くことができようになりました。全文索引にはリッチテキストフィールドのテキストも含むので、リッチテキストの中も検索できるようになるようです。添付ファイルを全文索引に含める設定をしていれば、サポートされているファイル形式のファイルにあるテキストも検索できそうです。


DQL では、検索する際に使うもの(サマリーフィールド or ビュー or 全文索引)がクエリーの書き方で変わります。


クエリーの例をいくつか挙げます。


たとえば、件名が"Request"のメールを探したい場合、

Subject = 'request'

とすると、データベース内の Subject という名前のサマリーフィールドから値が一致するもの探します。※このとき Subject でソートできるビューがDQLで使える仕様になっていればビューから探す場合もあります

検索に「All」ビューを使いたい場合、明示的にビュー名を指定することも可能です。

'All'.Subject = 'request'

このようにビューを検索で使う場合、Subject 列(Subjectは列のプログラム名)が最も左にあるソートされた列である、または Subject 列にクリックソートが設定されていることが必要です。

テクてくで発表した資料で示したとおり、NSFサーチ(サマリーフィールド)よりもビューを使った検索のほうがパフォーマンスは良好でした。


ここまで(これはほんの一部です)が V10 の DQL で使えた構文です。


V10 までは、フィールドまたはビュー列と、指定した値とが完全に一致する場合のみヒットしましたが、V11 では全文索引を使ってこれまでより柔軟な検索ができるようになりそうです。

V11 以降の DQL で全文索引を使うには、クエリーに Contains を用います。

たとえば先の例を、全文索引を使うよう書き換える場合、

Subject Contains ('request')

となります。ただし V10 では Subject の値が Complicated request の場合はヒットしませんでしたが、Contains を使えばヒットします。検索バーから全文検索する場合と違い、DQL では = と Contains は同じではないので注意が必要です。

それから、全文索引を使う場合には、アスタリスク * やクエスチョンマーク ? といったワイルドカードが使えます。

先の例では、件名が [Request] や REQUEST: といった文字列はヒットしますが、 Req: や はヒットしません。
もし Request 以外にも Req: や といった文言も検索したい場合、

Subject Contains ('req*')

とします。

クエスチョンマークは任意の1文字を表す場合に使います。例えば、'r??t' とすると root や REST はヒットしますが request はヒットしません。

また、件名だけでなく、本文など他のフィールドも含めて検索させたい場合、

Contains ('req*')

のように、フィールド名を指定せずに Contains を用いることで、全文索引にあるすべてのフィールドを対象として探すことができます。


さらに、複数の文言のどれかを含む文書を探したい場合は

Contains ('req*', 'server', 'replica')

のように括弧の中にカンマ区切りで指定できます。
また、

Contains all ('req*', 'server', 'replica')

のように contains の直後に all を加えることで、複数の文言のすべてを含む文書を探すことができます。


なお、前回のエントリで話題にしました「読者フィールドを使って閲覧制限した文書が全文検索にヒットする」事象は、DQL で全文索引を用いる場合にも同様であることを本ベータでは確認しました。※NSF サーチやビューを使った場合はヒットしません


V11 の DQL ではこういった構文の拡張の他、パフォーマンスのさらなる向上や、DQL準拠のビューを自動作成するといったプランもあるようです。次のリンクを参照ください。

https://www.cwpcollaboration.com/uploads/1/0/2/7/102707030/domino_app_dev__cr_.pdf

まだ未検証ですが、パフォーマンスについて書かれた12ページのグラフを見ると、検索スピードが物凄く速くなるようです。

さらに V11 の先には、複数のデータベースを対象にしたクエリーの拡張も計画にあるようですね。

今後の DQL にも注目していきたいと思います。

2019年9月9日月曜日

閲覧制限した文書も検索にヒットするのです

Notesでは「読者フィールド」と呼ばれる特殊なフィールドを用意しておき、閲覧させたい人やグループ、あるいは特定のロール(を付与されたACLエントリ)を読者フィールドへ埋め込むことで、文書の閲覧を制御することができることはご存知のとおりです。

読者フィールドに自分のアカウントや自分が所属するグループ、あるいは自分に付与されたロールが無い場合、その文書を閲覧することができません。

しかしながら、自分が閲覧できる文書がデータベースの中に1件も無い場合でも、データベースのプロパティには、閲覧できない文書を含む文書数が表示されます。エンドユーザーは何か見えない文書があることがわかるわけです。


さて、NotesDatabase クラスの Search メソッドや FTSearch メソッドを使う場合、検索条件に一致すれば、閲覧できない文書もヒットします。

これらのメソッドでは NotesDocumentCollection クラスのインスタンスが返ります。

もし、自分が閲覧できない文書がヒットした場合、それを GetFirstDocument や GetNextDocument を使って NotesDocument クラスのインスタンスが作られるのでしょうか。

次のようなコードを書きました。
Dim ss as New NotesSession
Dim db As NotesDatabase
Dim dc As NotesDocumentCollection
Dim doc As NotesDocument
Dim Query As String

Query = |[Subject] contains "request"|
Set db = ss.CurrentDatabase
Set dc = db.FTSearch( Query, 0 )
Set doc = dc.GetFirstDocument
While not doc is Nothing
    set doc = dc.GetNextDocument( doc )
Wend

このコードを実行して閲覧できない文書が検索にヒットした場合、GetFirstDocument や GetNextDocument で取り出した NotesDocument は Nothing になりませんが、UniversalID プロパティや NoteID プロパティの値は "" となりました。また NotesDocument クラスの Items プロパティの値はヌルになるため、フィールドの値へアクセスすることもできません。

つまり、見えない文書が存在することは分かるけれども、その詳細にはアクセスできないのです。

ここで注意しなければならないのは、読者フィールドを使ったデータベースでは、閲覧できない文書がヒットすることも想定しなければならない、ということです。

実は、私が書いたコードで「結果がなんか変だよ」と指摘があり調べたら、この想定がされてなかった、ということがあったのを思い出しました。

皆様も読者フィールドによる閲覧制限機能を後から追加するような変更を行う場合は、ぜひともご注意ください。

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」ができました。