まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:post

以前からなんとなくこの挙動に気付いてはいたのですが、データを受け取る側で加工することで回避できていたので真剣に原因や対処を考えていませんでした。今回とある機能をライブラリ化することになり、「受け取る側ではなく、送る側で対処が必要」になったので真面目に調査して、対処法含めて分かったのでブログに残しておくことにしました。

現象はこのブログエントリのタイトル通りなのですが、まず「何をするとどうなるか」を紹介します。例えば jQuery を使ったこんな内容のコードを実行したとします:
$.ajax({
  url: '/api/item',
  type: 'POST',
  data: { name: 'シャンプー', price: 1000 },
  success: function( result ){
    :
  },
  error: function( err, text, message ){
    :
  }
});

Ajax を使って /api/item に POST リクエストを実行する、という内容です。その際に { name: 'シャンプー', price: 1000 } という JSON データを送信しています。

これを以下のような Node.js のコードで受け取った場合を想定してみます(Node.js はあくまでコードの例であって、Node.js じゃなくても同じ現象が起こります):
var express = require( 'express' ),
    bodyParser = require( 'body-parser' ),
    app = express();

app.use( bodyParser.urlencoded( { extended: true } );
app.use( bodyParser.json() );

  :

app.post( '/api/item', function( req, res ){
  var data = req.body;
  console.log( data );

    :
});



Node.js ではよく使う方法ですが、Express (と body-parser)を使って "/api/item" に POST されたデータを受け取っています("var data = req.body;")。この例では受け取った内容をそのままコンソールに表示しています("console.log( data );")。

先ほどの POST データであれば、コンソール画面には
{ name: "シャンプー", price: 1000 }

という JSON データが表示されるような気がしますが、実際には
{ name: "シャンプー", price: "1000" }

という、price の値が送信した時の数値ではなく、文字列に変換されて送信されます。これが本ブログエントリのタイトルにも書いた『jQuery の $.ajax で JSON データを POST すると整数が文字列になる??』という現象です。

送る側だけでなく、受け取る側も自分達が実装しているのであれば受け取った JSON データの price の値を数値に変換するという手もあります:
//. data.price が文字列だった場合は数値に変換する
if( typeof data.price == 'string' ){
  data.price = parseInt( data.price ); 
}

最悪、この方法で対処できるかもしれませんが、受け取る側を変更できないという前提にした場合、この方法は使えなくなります。つまり「正しいフォーマットで送信する」必要がでてきます。

で、いろいろ調べた結果として、jQuery の Ajax 実行部を以下のように変更することで JSON データ内のフォーマットを壊さずに、JSON データのまま送信することができるようでした:
$.ajax({
  url: '/api/item',
  type: 'POST',
  contentType: 'application/json',
  dataType: 'json',
  data: JSON.stringify( { name: 'シャンプー', price: 1000 } ),
  success: function( result ){
    :
  },
  error: function( err, text, message ){
    :
  }
});

先ほどの例と違う部分を赤字にしていますが、要するに、
  • contentType: 'application/json' を追加する
  • dataType: 'json' を追加する
  • 送信する JSON データ全体を JSON.stringify() で文字列化する
という3つの変更を加えることでそのままのフォーマットで(受信側でも price は文字列の '1000' ではなく、整数の 1000 として)送信することができるようでした。


そういえば以前はこのスタイルで送っていたような気がしつつ、よりシンプルな方法でも(データ型が乱れるだけで)送れることに気付いて、それから省略形で送ってしまっていたような気がします。その結果、データのフォーマットがおかしくなっていた(が、あまり気にしなかった)ということだと思っています。 ともあれ、これで受け取る側を変更しなくても、正しいデータフォーマットのままで送信することができそうです。

jQuery の場合「正確に記述しなくてもある程度できちゃう」から、正確でなかった今までの方法が正しいと勘違いしていたようです。で、足りない部分もちゃんと記述すればできる、と。いい勉強になりました。



いま自分の空き時間を使って、過去に Node.js + Cloudant を使って(普通のウェブアプリケーションとして)作っていたウェブサービスの Node-RED 環境への移植に挑戦しています。要はサーバーサイド JavaScript 実行環境である Node.js を使って過去に開発したウェブアプリケーション(画面や REST API)を、Node-RED の HTTP リクエスト/レスポンスノードや、HTML テンプレートノードを使っても動くように移植することに挑戦している、ということです。それなりに実績のある Node.js アプリケーションを Node-RED 上でも動かすことができれば、プラットフォームとしての Node-RED のポテンシャルを証明することができるのではないか、と考えています。

これを具体的に進めようとすると、まずウェブ画面は(i18n とかを考慮しなければ)HTML テンプレートノードを使えば一通りのことはできると思っています。要するに HTML テンプレートの中に HTML や CSS, フロントエンド JavaScript を含めてしまえば、見た目や挙動含めて一通りの画面を作ることはできると思っています。

問題は REST API 部分です。例えばデータベース(今回は Cloudant)のデータを読み書きするインターフェースを REST API で用意しておき、フロントエンドの画面から REST API を呼び出すことでデータの読み書き更新削除を行うことができるようになります(理論上は)。この REST API を Node-RED の HTTP リクエストノードと、HTTP レスポンスノードと、function などのノードを駆使して必要な機能を実装することができるかどうかが移植の可否になりそうだと思っています。まあ普通にデータを読み書き更新削除検索・・・する程度であれば標準の Cloudant ノードの機能範囲内でできそうな感触を持っています。


さて、今とある Node.js + Cloudant 製ウェブアプリケーションの Node-RED プラットフォームへの移植を設計している中で1つの壁に当たってしまいました。上述のように「普通の」データの読み書きの REST API 化はさほど問題にならないのですが、Cloudant が持つ特徴を使った部分が普通のデータのように扱うことができず、一筋縄ではいかない内容でした。結論としてはなんとなく解決の目処はたったと思っているのですが、その内容と解決までの経緯を含めて以下にまとめてみたので、興味ある方はご覧いただきたいです。


さて、問題となった Cloudant が持つ特徴を使った部分です。Cloudant は NoSQL 型(JSON 型)データベースですが、特殊な JSON フォーマットで格納することでバイナリデータを格納することができます。またその格納されたバイナリデータを(Content-Type 含めて)出力することもできます。この機能を使うことで、例えば画像データを Cloudant に格納して、画像データとして出力する、といったことも可能です。この機能は Cloudant の各種 SDK からも便利に使えるよう関数化されていたりします。

実は Cloudant のこのバイナリデータ格納機能を使っている場合が Node-RED 移植をする上でのネックとなります。Node.js などのプログラミング言語で Cloudant を利用する場合(特に上述の機能を使ってバイナリデータを Cloudant に格納する要件が含まれる場合)、Cloudant の REST API や各種 SDK を使ってデータの読み書きを実装することになります。上述のバイナリデータの読み書きも同様です。バイナリデータを書き込んだり、バイナリデータを Content-Type 含めて(つまり画像データであれば画像データとして)取り出して出力したりできます。実際にアプリケーション開発の中でこの機能を使って実装していることは(個人的にはバイナリデータの格納先に Cloudant を使うことが多いので)珍しくありません。

しかし、この部分を Node-RED に移植できるか? となると話は変わってきます。まず Node-RED からは Node.js 向けの Cloudant SDK を利用することはできません。function ノードの中でがんばって  Cloudant の REST API を呼び出すような JavaScript を書けば Node-RED でできるかもしれません(認証情報をどのように管理するかの問題は残ります。またどうせ JavaScript でゴリゴリ書くというのであれば、そもそも Node-RED をプラットフォームに選択しない方が正しいような気もします)。 この問題を標準の Cloudant in/out ノードだけでバイナリデータの読み書きを扱うことはできないか? と読み替えて考えることにしました。


【方法1 正攻法】
そもそも何が正攻法なのか、という問題もありますが、実は標準の Cloudant in ノード(Cloudant にデータを格納するノード)はバイナリデータを格納することもできます。上述の Cloudant のバイナリデータ格納機能は単に JSON データフォーマットをうまい具合に指定することで実現しているので、データを格納する点までは少しの工夫で実現できるのでした。

ただし、この方法の問題点は格納時ではなく取り出し時にあります。標準の Cloudant out ノード(Cloudant からデータを取り出すノード)は _id 値を指定してデータを取り出すことはできるのですが、肝心のこの部分がバイナリデータ格納を意識することなく、普通に JSON データとして取り出してしまうことしかできないのでした。特殊なフォーマットで格納することでバイナリデータ格納を実現しているのですが、この特殊なフォーマットに合わせた取り出しができないため、書き込むことはできても読み出せない、という問題が残ってしまうのでした。。


【方法2 BASE64 エンコードを利用して独自実装】
なんとなく解決の目処が立っているのがこちらの方法です。データの読み書きそのものは Cloudant の標準ノードを使うのですが、扱うバイナリデータは格納前に BASE64 でエンコードして(標準 Cloudant in ノードで)格納します。そして取り出す際も普通に標準 Cloudant out ノードで取り出した後に該当部分を BASE64 でデコードします。最後に HTTP レスポンスノードの属性で Content-Type ヘッダを指定して、デコード結果(画像バイナリ)を返信する、という方法です。プログラミングによるカスタマイズを駆使した、いかにもプログラマーらしい方法ですが、こちらの方法であれば格納時だけでなく取り出し時にも問題なく実現できそうです。

試しにフローを作ってみました。Github でも公開したので良かったらこちらからフローをダウンロードするなどして後述の手順で試してみてください:
https://github.com/dotnsf/nodered_cloudant_binarydata_io


【方法2 サンプルフローの使い方】
このサンプルを使って、実際にバイナリデータ(画像データ)を Node-RED で読み書きできることを確認してみます。

まずは Node-RED 環境を用意します。個別に用意していただいても構いませんが、最終的に Cloudant データベースを用意する必要もあるので、IBM Cloud を使って用意する方法がおすすめです。なお IBM Cloud を使ってここに書かれた方法で Node-RED 環境を構築した場合は、始めから Cloudant-in / Cloudant-out ノードがインストールされた状態になっているので、後述のこれら Cloudant 関連ノードのインストールは不要です。無料のライトアカウントを使って構築することもできる内容なので、Node-RED 環境がない人が試す上でおすすめの方法ではあります。

上記以外の方法で(普通にインストールするなどして)Node-RED 環境を用意した場合は node-red-node-cf-cloudant ノードを別途インストールする必要があります。右上のメニューから「パレットの管理」を選択し、「ノードを追加」から "node-red-node-cf-cloudant" を検索して追加してください:
2021032101

2021032102


併せて IBM Cloud にログインして Cloudant サービスを追加して利用できるようにしておいてください。繰り返しますが、このあたりあまり詳しい自信がない場合は上述の方法で IBM Cloud 環境内に Node-RED 環境を Cloudant データベースや Cloudant ノードなどとまとめて用意するのがおすすめです。


Node-RED が準備できたら、上述の Github リポジトリを使ってサンプルのフローを構築します。この flow.json ファイルがサンプルのフローそのものです。リンク先のテキスト内容をまとめてコピーし、Node-RED の右上メニューから「読み込み」を選択します:
2021032103


読み込みのダイアログで「クリップボード」を選択し、コピーしていた内容をペーストします。そして「新規のタブ」を選択し、最後に「読み込み」ボタンをクリックします:
2021032104


するとこのようなフロー画面が再現されるはずです:
2021032105


このままだとまだ2つの Cloudant ノード(画面上では "mydb" と表示されている2つの水色ノード)が未接続で使えません。どちらかをダブルクリックして設定ダイアログを表示します。すると Service 欄が一瞬だけ空のまま表示されますが、IBM Cloud の Node-RED 環境であれば接続済みの Cloudant サービスを見つけて接続してくれます。Service 欄に Cloudant サービス名が表示されたら「完了」ボタンをクリックします(もう1つの Cloudant ノードも同様にして Service 欄が埋まった状態にします):
2021032106


このように2つの Cloudant ノードの右上に表示されていた赤い印が2つとも消えればサンプルを動かすための準備は完了です。画面右上の「デプロイ」ボタンでデプロイして動作前の準備は完了です:
2021032101


改めてこのタブを見ると、3つの HTTP リクエストを処理するフローが定義されています:
#HTTP リクエスト処理内容
1GET /home画像ファイルアップロード画面
2POST /file画像ファイルアップロード処理
3GET /file(?_id=XXXX)アップロードした画像ファイルを画像として取り出す処理


1番目の GET /home は後述の 2 と 3 の動作を確認するための UI として、ファイルを指定してアップロードできる画面を表示するものです。実際に /home へアクセスすると、以下のような画面が表示されます:
2021032201


非常にシンプルなファイルアップロード機能を持ったページです。「ファイルを選択」ボタンを選んでローカル PC からファイル(今回は画像ファイル)を選択して「送信」ボタンをクリックです。「送信」すると、2番目 POST /file が実行されて、選択したファイルが Cloudant に格納される、というものです。

ここで試しに以下の画像ファイルを指定してアップロードしてみます(お好きな画像で試してください):
dotnsf_logo_200x200


画像ファイルを指定して「送信」します:
2021032202


こんな感じの HTTP POST の結果が表示されます(実際のアプリでは AJAX を使うなどしてこの結果をそのまま表示しないようにします):
2021032203


この後に Cloudant のダッシュボードなどから mydb データベースの中を確認するとデータが1件追加されているはずです:
2021032204


表示を JSON 形式などに切り替えると、格納されたデータファーマットも確認できます(type に画像フォーマット、data に base64 でエンコードした画像バイナリデータが格納されています):
2021032205


このデータの id 値を確認します(上図だと c7c3eb8e3b9ac0fffcd45c1beea6c62a )。この値と3番目の GET /file を使って格納された画像を表示してします。ウェブブラウザで /file?_id=(id の値) にアクセスして、アップロードした画像が表示されることを確認します:
2021032206
(↑アップロードした画像が復元できた!)


Node-RED を使ってバイナリ(画像)ファイルを Cloudant に格納し、また Node-RED から画像を復元することも実現できることがわかりました。


【方法2 解説】
Node-RED の HTTP リクエストでバイナリデータを格納したり、Node-RED の HTTP リクエストで格納したバイナリデータを取り出すことができる、ということがわかりました。以下はこれを実現している上記フローの解説です。

まず画面 UI である GET /home ですが、これはごく普通に enctype="multipart/form-data" を指定したフォームを定義しているだけです。テンプレートノードの中身は以下の内容の HTML です:
<html>
<body>
<form method="POST" action="/file" enctype="multipart/form-data">
<input type="file" accept="image/*" capture="camera" name="image" id="image"/>
<input type="submit" value="送信"/>
</form>
</body>
</html>

次に POST /file の各ノードを説明します。まず HTTP in ノード(POST /file と書かれたノード)はファイルアップロードに対応する処理を行うため「ファイルのアップロード」にチェックを入れている点に注意してください:
2021032207


また直後の function ノードの内容は以下のようになっています。アップロードされたファイルは msg.req.files に配列で格納されます(今回はファイル1つだけですが、配列の0番目に格納されます)。その mimetype と buffer を取り出し、buffer を base64 エンコードして msg.payload に格納し直して、最後にタイムスタンプを追加する、という処理を行っています(Cloudant データベースにはこのフォーマットで格納されていたはずです):
2021032208
//. アップロードしたファイルを base64 エンコーディング
var type = msg.req.files[0].mimetype;
var img64 = new Buffer( msg.req.files[0].buffer ).toString( 'base64' );

//. 独自フォーマット化
msg.payload.type = type;
msg.payload.data = img64;

//. タイムスタンプを追加
msg.payload.timestamp = ( new Date() ).getTime();

return msg;


この function ノードで処理された msg.payload の内容を Cloudant out ノードが受け取って格納します。このノードでは「Only store msg.payload object?」にチェックを入れて、ヘッダ情報などを格納しないようにしています。これで指定したバイナリファイルを(base64 エンコードして)Cloudant に格納する(同時に _id が割り振られます)、までの処理を実現しています:
2021032209


最後に GET /file(?_id=XXXX) のノードを紹介します。まず Cloudant in ノードでは特別な処理は行っておらず、パラメータとして与えられた _id を使って Cloudant の mydb 内を検索して結果を返す内容にしています:
2021032201


直後の function ノードでは mydb から取り出した結果を画像に戻す処理をしています。上述の function ノードの逆を行う形で、msg.payload.data の値を base64 デコードして画像バイナリに戻して msg.payload に代入し直しています:
2021032202
//. base64 エンコードされているバイナリデータをデコード
if( msg.payload && msg.payload.data ){
  msg.payload = new Buffer( msg.payload.data, 'base64' );
}

return msg;


その結果を HTTP レスポンスノードに渡して処理は終了です。が、このノードでは HTTP ヘッダをカスタマイズし、"Content-Type: image/png" を付けています。つまり直前の function ノードで取り出した画像のバイナリを画像(image/png)として送信するための処理を最後に加えています:
2021032203


これらのノードや処理を組み合わせることで Node-RED の HTTP リクエストからバイナリデータを Cloudant に格納したり、格納したデータからバイナリデータを取り出して Content-Type ヘッダを付けて返す、といった一連の処理を実現していました。


この例は Cloudant にバイナリデータを格納する場合のサンプルでしたが、おそらくほぼ同様の方法で他のデータストアにも応用できると思っています。



久しぶりの Java プログラミングの機会があり、これまた久しぶりにサーブレットを作りました。POST されたデータを受け取って、バイナリデータを生成して、Content-Type をつけてストリームで返す、というものです。

この POST データの受け取り方をどうするかで(少しだけ)迷ったのですが、変数もその型も数も不定で受け取るような仕様だったので、深く考えずに JSON データを受け取ることにしました。普段の Node.js の時は特殊な事情がない限り JSON で受け取ることにしているので、その延長というか「まあイマドキは JSON だよね」くらいに考えていました。

・・・が、これが意外と苦戦。 誰かの参考になれば・・・、と思って、以下実際に作ったサーブレットの実装を紹介します。

まず最初はクライアント(呼び出し)側は普通にこんな感じの実装にしました。jQuery の AJAX を使ってタイムスタンプ値を含む JSON オブジェクトをポストしています。これを受け取って処理するサーブレット(/postdata)を作るのが今回の目的となります:
  :

$.ajax({
  type: 'POST',
  url: './postdata',
  data: { timestamp: ( new Date() ).getTime(), body: 'ハローワールド' },
  success: function( result ){
    console.log( result );
  },
  error: function( err ){
    console.log( 'error' );
  }
});
:

そしてそのサーブレットのコード部分が以下です:
public class PostdataServlet extends HttpServlet {
	
  @Override
  protected void doPost( HttpServletRequest req, HttpServletResponse res )
throws ServletException, IOException {
req.setCharacterEncoding( "UTF-8" ); try{
//. JSON テキストを全部取り出す BufferedReader br = new BufferedReader( req.getReader() ); String jsonText = br.readLine(); jsonText = URLDecoder.decode( json, "UTF-8" ); //System.out.println( jsonText );
//. JSON オブジェクトに変換 JSONParser parser = new JSONParser(); JSONObject jsonObj = ( JSONObject )parser.parse( jsonText );
//. JSON オブジェクトから特性の属性を取り出す
String body = ( String )jsonObj.get( "body" );
: }catch( Exception e ){ e.printStackTrace(); } } }

要は req.getParameter() などを使って特定のパラメータ値を取り出すわけではなく、req.getReader() を使ってポストされてきた全データ(今回の場合は JSON オブジェクトをテキスト化したもの)を受け取る必要があります。そして受け取ったテキストデータを(上記の例であれば JSON-Simple ライブラリを使って)JSON オブジェクト化した上でサーブレット内で取り扱う、という手法です。


・・・単純に JSON データを扱いたかっただけなんだけど、こんなに面倒だったっけ?

 

このページのトップヘ