Tagging Serviceとvte.cx engine

O.概要

  • Tagging Serviceは、コンテンツやデータの管理機能に特化したフレームワークです。Webアプリケーションのための本質的な機能を提供します。
  • vte.cx engine(ブイテックス エンジン)は、Tagging Service フレームワークをベースにしたBaaS(Backend as a Service)です。

O1.REST APIによるリソース管理

  • ツリー構造の仮想フォルダ管理とREST APIによる直感的なデータアクセスが特長で、データストアのリソースを、JSONXMLMessagePackなどの様々な形式に変換して取りだすことができます。
  • リソースはATOM Feed/Entryで仮想フォルダ管理します。アクセス制御、共有設定、暗号化といった設定が可能です。
  • データストアにオブジェクトが直接保存されるため、O/Rマッピングなどの変換は必要ありません。また大小比較や前方一致検索など様々な検索が可能です。
  • WebSocket、MobilePush、メール通知などのイベント通知機能、また、サーバサイドJavaScriptによるカスタムロジックの組み込みが可能です。
resource

(※) ソーシャルログインは次のバージョンより提供予定です。

O2.ソフトスキーマと複合文書管理

  • テンプレートに項目を記述することでスキーマを定義します。テンプレートは柔軟なソフトスキーマであり、サービス運用中であっても自由に項目の追加ができます。
  • 項目ごとにバリデーションルールACL(Access Control List)Index暗号化などを記述します。
    • バリデーションルールには必須項目や値の範囲、正規表現などを設定します。
    • ACLにはユーザやグループ名と読込・書込権限などを設定します。
    • Indexを指定することで大量のデータでも高速に検索できるようになります。
    • 暗号化により情報を盗み見から守ることができます。
    entrycontents
  • HTMLやJavaScript、写真/画像などの静的コンテンツが扱えるため、PCやモバイルのランディングページ等のWebページCMS機能として使用できます。
  • これらはACL設定によりユーザやグループ単位での公開/非公開設定が可能です。

O3.マルチテナントアーキテクチャーと可搬性

  • 複数のサービスが複数のノードで動作するマルチテナントのアーキテクチャーを採用しています。ユーザに紐づくデータ管理を水平分散化した複数のノードによりスケーラブルに管理します。
  • ユーザID・パスワード認証の他、ソーシャルログイン機能により、Google AcountやTwitter、Facebookなどのアカウントを使ってログインできます。(※)
  • 基本的に疎結合であり、データストアなどの下位のレイヤーをアプリケーションから隠蔽するため、業務アプリケーションは高い可搬性をもちます。
    • 例えば、BaaSで動作する同一の業務アプリケーションをオンプレミスでも動作させることができます。 architecture

R.リソース管理

ここでは、Tagging Serviceにおけるリソース管理について説明します。

R1.Keyと階層管理

  • Tagging Serviceは、様々なコンテンツをATOM Feed/Entryのリソースとして管理します。リソースはURLにマッピングされ、REST APIによりURLで指定されたリソースに対して読み書きできます。
  • マッピングしたURLをKeyと呼びます。Keyは「/path/to」のようなパス形式となります。
  • Keyに含まれる名前には英数字と$、_(アンダースコア)が使えます。
  • Keyは階層にすることが可能であり、例えば、/foo/barとすると、foo の子にあたるbarを示すことができます。階層は1000階層まで持つことができ、子は無限に持つことができます。
  • Keyのルートはサービス名で、その下にユーザ定義のフォルダという形となります。サービス名は省略できます。フォルダとEntryの違いはなく、子を持つEntryを便宜上フォルダと呼んでいます。
    • フルパス表記
           /@{サービス名}/{ユーザ定義のフォルダ}/・・/{ユーザ定義のEntry}
    • 通常は以下のように@サービス名を省略します。
           /{ユーザ定義のフォルダ}/・・/{ユーザ定義のEntry}        
    • 以下のようにuid配下のフォルダとすることでそのユーザの管理下とすることができます。uidはユーザ識別子です。
      • こうすることでノード振り分け対象となりシステム内で分散化が可能になります。
      • キーの第二階層がuidでなく数値以外で始まる場合は常に特定のノード(サービス代表ノード)にデータが作成されます。
             /{uid}/{ユーザ定義のフォルダ}/・・/{ユーザ定義のEntry}
  • 実際にアクセスするURLパスは、/d/xxx という形になります。/dはデータにアクセスするためのAPIと考えてください。
    • (/apiではなく/dとしているのは対象がリソースデータであることを示す意味があります。/d以外にも、/s(サーバサイドJavaScript)、/x(excel変換)などがあります。)
  • 例えば以下は、curlコマンドを使って、/registrationフォルダのエントリ1を取得する例です。?eはentryを意味します。
       curl -H "Authorization: Token {Accesstoken}" http://sample.vte.cx/d/registration/1?e
    {
        "feed": {
            "entry": [
                {
                    "favorite": {
                        "food": "りんご"
                    },
                    "author": [
                        {
                            "uri": "urn:vte.cx:created:11"
                        },
                        {
                            "uri": "urn:vte.cx:updated:11"
                        }
                    ],
                    "id": "/registration/1,16",
                    "link": [
                        {
                            "___href": "/registration/1",
                            "___rel": "self"
                        }
                    ],
                    "published": "2015-10-16T10:58:10.956+09:00",
                    "updated": "2015-10-16T23:44:58.176+09:00"
                }
            ]
        }
    }

R2.ATOM項目とユーザ定義項目

  • データはATOM( RFC4287)によって表現されます。XMLやJSON、MessagePackなどに変換できます。
  • データはリスト形式でfeedの中に複数のentryが入る形になります。
  • ATOM項目のうち、author、id、published、updatedなどは自動で値がセットされますが、title、subtitle、summary等はユーザによって自由にセットできます。
  • contentは内部コンテンツの格納用として使われます。
  • linkは別名(alias)や外部コンテンツのリンクとして使われます。
  • contributorにはACLを管理するために使われます。
  • rightsは暗号化される項目であり設定情報などで使われます。
  • スキーマに記述することでユーザ独自の項目を定義できます。上記の例では、favorite.foodがユーザ定義項目になります。

R3.Entryの構造

  • リソースの最小単位であり、1件のデータが1Entryになります。
  • <link>タグのrel="self"属性のhref属性がEntryの参照を表すKeyです。(以下、link selfと記述します)
  • Entryは一つのidを持ちます。これは、システム全体で一意となるように、KeyとRevisionを組み合わせた形をしています。
    • <id>タグの値には、Entryの実体を示す一意の値(Key,Revision)が入ります。
    • RevisionはEntryの更新回数です。
  • link selfにはKeyが入ります。これはGETを実行する際に指定したURLと同じものになりますが、別名(alias)KeyでGETするとidとは異なるKeyの値となりますので注意してください。

R4.リクエストヘッダとレスポンス形式指定パラメータ

  • 特に記載がない場合、デフォルトの戻り値はJSON形式ですが、URLパラメータに?xや?mオプションを指定することでXMLやMessagePack形式を指定できます。どのような形式に変換されてもデータ構造は同じ形になります。
  • x : XML文字列 (Content-Type : text/xml)
  • m : Messageack形式バイナリをDeflate圧縮 (Content-Type : application/x-msgpack)
    • リクエストヘッダに「Content-Encoding: deflate」があればリクエストデータは圧縮されていることを示します。
    • リクエストヘッダに「Accept-Encoding: deflate」があればレスポンスデータを圧縮します。
  • 上記パラメータが無い場合はJSON文字列 (Content-Type : application/json)
    • JSON文字列返却はXHR通信からのみ受け付けるようにしているため、リクエストヘッダに「X-Requested-With: XMLHttpRequest」が設定されていなければエラーを返却します。(ステータス417)
    • セキュリティー上の理由でブラウザに直接JSONを表示させないようにしています。
  • POSTやPUTのペイロード(リクエストのBody)はraw形式のみ許可しています。送信する際はXHR通信である必要があります。フォームデータ(HTML Form)送信はCSRF対策のため受け付けられません
レスポンスの形式
  • HTTPステータスコードによりエラーなどを判断してください。詳しくは、HTTPステータスコードとメッセージを参照してください。
  • POSTやPUTなどの場合は基本的なレスポンスの形式は以下のような形になります。
    <feed>
      <title>{メッセージ}</title>
    </feed>
  • GETの場合、entryもしくはfeedが返ります。以下はentryをjson形式で取り出す例です。デフォルトの表現形式はJSONになります。?xでxml、?mでMessagePackになります。?eはentryを意味します。
    • GET /9/imgs/mokuren?e
    {
       "feed":{
          "entry":[
             {
                "itemcode":"001",
                "typeno":"",
                "size":"中",
                "color":"紫",
                "design":"",
                "brand":"",
                "functions":"",
                "styles":"",
                "manufacturer":"",
                "material":"",
                "country":"日本",
                "expiration_date":"",
                "author":[
                   {
                      "uri":"urn:vte.cx.net:created:9"
                   },
                   {
                      "uri":"urn:vte.cx:updated:9"
                   }
                ],
                "content":{
                   "______text":"花がランに似ていることからモクランという呼び方もある"
                },
                "id":"/9/imgs/mokuren,4",
                "link":[
                   {
                      "___href":"/9/imgs/mokuren",
                      "___rel":"self"
                   },
                   {
                      "___href":"/9/albums/folder1/mokuren",
                      "___rel":"alternate"
                   },
                   {
                      "___href":"blobkey12345",
                      "___rel":"related",
                      "___type":"blob",
                      "___title":"flower.jpg",
                      "___length":"1"
                   }
                ],
                "published":"2014-02-23T13:45:41.001+09:00",
                "summary":"落葉低木",
                "title":"モクレン",
                "subtitle":"木蘭",
                "updated":"2014-02-23T13:56:07.796+09:00"
             }
          ]
       }
    }
  • 先頭___のものはXMLに変換すると属性となる項目です。また、______textはXMLのテキストノードに対応します。
  • 以下はxmlの取得例です。xを指定しないとJSONが返ります。
    • GET /9/imgs/mokuren?e&x
    <?xml version="1.0" encoding="UTF-8" ?>
    <feed>
      <entry>
        <itemcode>001</itemcode>
        <typeno></typeno>
        <size>中</size>
        <color>紫</color>
        <design></design>
        <brand></brand>
        <functions></functions>
        <styles></styles>
        <manufacturer></manufacturer>
        <material></material>
        <country>日本</country>
        <expiration_date></expiration_date>
        <author>
          <uri>urn:vte.cx:created:xxx@gmail.com</uri>
        </author>
        <author>
          <uri>urn:vte.cx:updated:xxx@gmail.com</uri>
        </author>
        <content>花がランに似ていることからモクランという呼び方もある</content>
        <id>/9/imgs/mokuren,4</id>
        <link href="/9/imgs/mokuren" rel="self"/>
        <link href="/9/albums/folder1/mokuren" rel="alternate"/>
        <link href="blobkey12345" rel="related" type="blob" 
         title="flower.jpg" length="1"/>
        <published>2014-02-23T13:45:41.001+09:00</published>
        <summary>落葉低木</summary>
        <title>モクレン</title>
        <subtitle>木蘭</subtitle>
        <updated>2014-02-23T13:56:07.796+09:00</updated>
      </entry>
    </feed>

R5.idとバージョン管理、楽観的排他制御

  • idが同じということはそのEntryのデータが完全に同一であることを意味します。
  • Entryを登録すると、<id>タグに値を自動的に設定します。{Key},{Revision}の形式
    • 更新時、idで楽観的排他チェックを行います。つまり、元のRevisionと比較して値が異なれば既に更新されていることになり処理は楽観的排他エラーとなります。ただし、更新するEntryにidが含まれていない場合は楽観的排他チェックを行いません。
    • 削除時、指定されたRevisionと更新前のものを比較することで楽観的排他チェックを行います。Revision指定が省略された場合はチェックを行いません。

R6.トランザクション処理

  • Feedに含まれるEntryは1トランザクションで実行されます。
  • 一貫性 で説明しているようなトランザクション処理が可能です。
  • 例えば、以下のような在庫エントリのカウントを減らし(1件目のentry)、受注エントリに登録する(2件目のentry)処理を1トランザクションで実行することができます。
    • 在庫A001のcountを3に減らしていますが、もしこのエントリが更新されていれば楽観的排他制御でエラーになりロールバックされます。(revisionが異なる)
      {
          "feed": {
              "entry": [
                  {
                      "A001": {
                          "count": "3"
                      },
                      "id": "/stock/book,3",
                      "link": [
                          {
                              "___href": "/stock/book",
                              "___rel": "self"
                          }
                      ]
                  },
                  {
                      "item": {
                          "book": "A001"
                      },
                      "id": "/order/1,16",
                      "link": [
                          {
                              "___href": "/order/1",
                              "___rel": "self"
                          }
                      ]
                  }
              ]
          }
      }
Feed、Entryの更新(POST、PUT)におけるトランザクション処理
  • 処理シーケンス

    1. トランザクションを開始

    2. データを1件get(READロック:LockMode.READ_COMMITED)

    3. getしたデータを編集

    4. 編集したデータをput

    5. Feed(Entryを複数件包含する)の場合、2.-4.を繰り返す

    6. トランザクションをcommit

  • 考慮点
    • ロックはデータのレコードにアクセスした時点で行われる。
    • デットロックが発生してもLockTimeoutエラーではじかれるため、リトライにより更新が可能。
    • また、読込のパフォーマンスを優先するためREADロックにしている。
      • WRITEロックでは読み取り(get)操作で排他ロックを取得するため、並列読取り処理時のパフォーマンスが低下することが懸念される。そのため、ロック競合やデッドロックの影響が小さい箇所においては通常のReadLockを使用する方がパフォーマンスはよくなる。
AllocIdsなどカウンタ類の更新におけるトランザクション処理
  • 処理シーケンス

    1. トランザクションを開始

    2. データを1件get(WRITEロック:LockMode.RMW)

    3. getしたデータを編集

    4. 編集したデータをput

    5. トランザクションをcommit

  • 考慮点
    • カウンタは競合がよく発生するためWRITEロックを採用した。この方がリトライするよりパフォーマンスがよくなる。

R7.別名(alias)

  • Entryにlink rel=alternateを付与することで別のURLにマッピングすることが可能です。これを別名(alias)と呼んでいます。LinuxのシンボリックリンクやWindowsのショートカットフォルダのような機能です。
  • 例えば、ECサイトのカテゴリ表示において、家電/パソコン/androidや、家電/スマートフォン/androidといったように、androidを異なる複数のカテゴリに関連付けできます。androidは一つのentryであるにもかかわらず複数のパスから参照できます。
  • フォルダ移動などで上位階層が変わったときも、aliasの書き換えだけで下位Entryを変更しないで済むという利点があります。
  • 子要素が存在するフォルダにaliasを設定するとaliasのパスからもその子要素にアクセスできるようになります。
  • 共有情報のインデックスとしてalias情報のキャッシュ(シャドウ)が各ノードに展開されるためリモートの情報(他ノード)へも最短のパスでアクセスします。

R8.シャドウと他ノード検索

  • 分散環境において情報共有を効率よく検索するため他ノードのalias情報のキャッシュをを自ノードに保持します。これをシャドウと呼びます。
  • シャドウにより他ノードのEntryを自ノード内で高速に検索できるようになります。また、シャドウを介して他ノードのフォルダ検索ができるようになります。
  • 他ノードのEntryが更新されるとシャドウも更新します。これは同期更新ではなくEventuallyな非同期更新です。また、冪等性を保証します。
  • 自ノードから直接シャドウを更新することはできません。更新はあくまで他ノードにあるオリジナルのEntryに対してのみ行えます。
他ノードのフォルダ検索の例
  • aliasをたどる場合shadow1.png
  • シャドウをたどる場合shadow2.png
他ノードの登録・更新・削除について
  • 他ノードのEntryを登録・更新・削除する場合は、ユーザはまず自分の担当ノードにリクエストします。ノード内でEntryが他ノードと判定されれば、ノードが担当ノードに対し更新リクエストを投げます。
  • 自ノードにシャドウがある場合は他ノードの更新処理が実行された後でシャドウ更新を実行します。

R9.管理情報の共有

  • ノード情報などのシステム管理情報は管理サービスに登録された情報を正とします。
    • システム管理情報(/@/_から始まるエントリー)は全ノードにシャドウを作成します。
  • サービス管理情報(「/@{サービス名}/」エントリー)はサービスの代表ノードから各ノードにシャドウを作成します。
    • サービスの各ノードではシャドウを参照することで情報にアクセスします。

R10.採番

  • Entryとは別に採番カウンターを持つことができます。在庫数など数の管理に使うことを想定しています。
  • 詳しくは、採番処理 を参照してください。

R11.リダイレクト

  • <link rel="related">のhrefで示すURLにリダイレクトします。
  • /9/imgs/mokurenエントリに以下のタグが付いているとします。
    <link rel="related" title="flower.jpg" href="/img/flower.jpg" /> 
  • 以下のように実行することで/img/flower.jpgにリダイレクトされます。URLパラメータの_related=もしくは、+表記が使えます。
    GET /9/imgs/mokuren?_related=flower.jpg
    GET /9/imgs/mokuren+flower.jpg

R12.内部コンテンツ

  • <content>タグ内にテキストデータやバイナリデータを格納することができます。これを内部コンテンツと呼びます。
  • 内部コンテンツは、HTMLやCSS、JavaScript、画像などを想定しています。
    • 内部コンテンツが格納されている場合、?eや?fなどのパラメータを指定せずにブラウザでURLを指定してGETするとfeedではなく内部コンテンツそのものを返します。例えば、GET /foo/bar.html を実行すると実際にHTMLページを表示します。(/foo/bar.htmlのcontentにhtmlが格納され、type属性にtext/htmlがセットされている前提)
    • 同時にcontent-typeヘッダもセットして返します。
    • Content-Typeが「text/」で始まる場合、文字コンテントとみなし、そうでない場合、バイナリとみなします。バイナリはBase64 エンコーディングして格納、表示する際は逆にBase64 デコーディングします。
  • 内部コンテンツは1entryの最大サイズ1MiB以内になります。(contentを含むentryが1MiBを超えない)
  • ?_contentパラメータを付けてPUTすることでコンテンツ(payloadの中身)を直接登録することができます。コンテンツは<content>タグ内に格納します。
  • 高速化のためETagによるレスポンスコントロールを行っています。つまり、前回参照したコンテンツに更新がなければ、304 Not Modifiedステータスを返します。
  • 以下のシェルスクリプトはローカルのコンテンツをまとめてサーバにアップロードします。第一引数にアクセストークン、第二引数にローカルのフォルダ、第三引数にURL、第四引数にcontentを指定します。
    ./rxcp.sh xxxx folder http://xxx.vte.cx/d/1/folder content  (folder -> http://xxx.vte.cx/d/1/folder)
    • 第四引数のcontentを省略した場合、コンテンツではなくデータ(feedやentry)を登録します。

R13.外部コンテンツ

  • Google Cloud Storageなどの外部ストレージ等に格納されているコンテンツデータを扱えます。
  • 格納できるコンテンツのサイズに制限はありません。
  • contentタグのsrcに外部コンテンツのURLが設定されます。

RX.特別なリソース

  • 第一階層のentryで_から始まるものは特別なリソースとして扱われます。ここでは特別なリソースについて説明します。

RX1.設定情報

  • /_settings/propertiesのrightsタグには以下のような情報を設定できます。
  • ここで設定されていないものはシステムの設定情報(デフォルト)が使用されます。
    _entry.number.default : エントリーGET時のデフォルト最大数 [100]
    _ignore.condition.{連番} : 検索条件除外値。ここに指定した値はGETでクエリパラメータに指定しても検索条件とならない。
    _errorpage.{適用順}.{エラーページselfid}={PathInfoの正規表現} : エラー画面表示URLパターン(正規表現)
    _aws.sns.push.self : 更新者自身にpush通知を行うかどうか(Amazon Web Service SNS mobile push) [false]
    _rxid.minute : RXID(WSSE)有効時間(分) [120]
    _rxid.counter.{連番}.{回数} : 同じRXIDを使用しても指定回数まで許可されるURLパターンを指定。
    _session.minute : セッション有効時間 [30]
    _aws.sns.push.self : 更新者自身にpush通知を行うかどうか(Amazon Web Service SNS mobile push) [false]
  • メール送信に必要な情報も設定できます。
    _mail.from.personal : EMailの送信元名
    _mail.from : Emailのfrom
    _mail.transport.protocol : smtpsなどのメール送信プロトコル
    _mail.password : メール送信アカウントのパスワード
    _mail.smtp.host : メール送信ホスト
    _mail.smtp.port : メール送信ポート
    _mail.smtp.auth : true/false
  • /_settings/templateにはスキーマテンプレートを定義します。詳しくは、テンプレートによるスキーマ定義 を参照してください。
  • /_settings/adduserにはユーザ追加時(?_adduser)に送られるメール本文を定義します。
  • /_settings/passresetにはパスワードリセット時(?_passreset)に送られるメール本文を定義します。

RX2.HTMLコンテンツ

  • /_html配下のエントリーはHTMLコンテンツとして扱います。CSSやJavaScript、画像なども同様に配下のエントリーに含まれます。
  • rewriteの機能により、/_html は / にマッピングされます。
  • 以下にvtecxblankプロジェクトに含まれる主なHTMLコンテンツについて説明します。
ログインページ(/login.html)
  • カストマイズして使うことができるログイン画面です。
  • パスワードはハッシュ化されたものが送信されます。
  • 2回以上認証失敗するとCaptchaが表示されます。
エラーページ(/error.html)
  • ログインしないで/dのデータにアクセスした場合など、エラー時にリダイレクトします。
  • /_settings/propertiesのrightsタグに_errorpage.uriを記述することでリダイレクト先を設定できます。
  • CookieのERROR_STATUSにHTTPステータスコード、ERROR_MESSAGEにエラーメッセージをセットします。
    • このCookieは10秒間だけ有効です。

RX3.グループ

  • /_group配下にはグループが管理されます。詳しくは、グループと共有 を参照してください。

RX4.ログ

  • /_log配下にログが降順(最新が先頭)で記録されます。
  • 以下の項目に情報がセットされます。
    • updated : 更新日時
    • title : タイトル
    • subtitle : サブタイトル
    • summary : 内容

C.アクセスコントロール

  • アクセスコントロールを特定のユーザの単位で、あるいはグループ単位でリソースに設定できます。
  • ここでは主にフォルダのアクセスコントロール、フォルダACLについて説明します。項目ACLについてはIndex、暗号化、項目ACLを参照してください。

C1.フォルダACL

  • フォルダにACLを設定することで自身および配下のEntryについてアクセスコントロールが有効になります。
  • フォルダACLは、指定されたEntryから有効です。配下のEntryにACLの設定がある場合、上位階層で設定されたACLではなく配下のACLが有効となります。
    • 登録の場合、親階層以上のACLをチェックします。
    • 検索、件数取得、更新・削除の場合、自身の階層を含む上位階層のACLをチェックします。
      • ただし、Feed検索では、検索前に自階層を含む上位階層のACLをチェックします。また検索後は、検索結果のうち参照権限がないEntryを結果に含めないように返します。
      • 件数取得の場合、配下のEntry件数を返しますが、Feed検索のような参照権限チェックは行いません。
    • 対象の階層で権限が存在しない場合、階層を1つずつ上がって権限チェックしていきます。
    • 別名(alias)の登録は、aliasのKeyについて登録(C)権限がある場合のみ実行可能です。削除については削除(D)権限がある場合のみ実行可能です。
      • aliasに対してGET, PUT, DELETEを実行した場合、aliasのKeyについてACLチェックします。
  • ACLはEntryのcontributorタグに以下の形式で指定します。GroupKeyについては、グループキー(GroupKey)を参照してください。
    <contributor>
        <uri>urn:vte.cx:acl:{uid|*|+|-|GroupKey},{C|R|U|D|E|.|/}</uri>
    </contributor>
    • uriの第二項目(CRUDE./)は複数指定可です。そのままつなげて指定します。
    • CRUDのいずれかの指定が必須です。(Eまたは「.」「/」のみの指定は不可)
    • contributorは、項目ACLとして、自身とサービス管理権限(/_group/$admin)のRW権限がデフォルトで付いています。Index、暗号化、項目ACLを参照してください。
  • ACLの種類 (複数指定可能)
    • C : 登録処理を許可
    • R : 検索処理を許可
    • U : 更新処理を許可
    • D : 削除処理を許可
    • E : ReflexContext()のみデータ操作可でHTTPからのデータ操作が不可となる。
    • . : Own このEntryのみ指定された権限が適用される。配下のEntryには適用されない。
    • / : Low このEntryより配下について、指定された権限が適用される。指定したEntry自体には適用されない。
    • Own.Low/いずれも設定されていない場合はデフォルトで両方(./)が付く
  • 権限のスコープについて
    • 数字 : ログインユーザのuidを指定します。先頭と末尾にワイルドカード(*)が指定できます。
    • * : ログインしていないユーザを含むすべてのユーザに対しACLが適用されます。
    • + : ログインしているすべてのユーザに対しACLが適用されます。
    • - : link selfまたはaliasのユーザでトップEntryのユーザ(下の例をご覧下さい)
      • link self、または、aliasの操作対象Entryのものが適用されます。
      • link selfからの階層によるKeyで参照できるものも適用されます。
    • スラッシュ(/)で始まるもの : GroupKey

C2.Low権限の例

  • 以下のようなユーザ1(low権限なし)とユーザ5(low権限あり)がある場合を考えます。
    <entry>
      <id>/1/test_low,1</id>
      <link rel="self" href="/1/test_low" />
      <contributor>
        <uri>urn:vte.cx:acl:1,CRUD</uri>
      </contributor>
      <contributor>
        <uri>urn:vte.cx:acl:5,CRUD/</uri>
      </contributor>
    </entry>
    <entry>
      <link rel="self" href="/1/test_low/t001" />
      <title>テストLow-001</title>
    </entry>
  • ユーザ5が可能な操作は以下となります。
    GET    /1/test_low      (配下のEntry一覧)
    GET    /1/test_low/t001?e
    POST   /1/test_low/t005
    POST   /1/test_low/# (自動採番、実際の指定時は#を付けない。)
    PUT    /1/test_low/t001
    DELETE /1/test_low/t001
  • ユーザ5に権限のない操作は以下となります。
    GET    /1/test_low?e   (自身のEntry参照)
    PUT    /1/test_low
    DELETE /1/test_low

C3.Own権限についての例

  • 以下のようなユーザ1(Own権限なし)とユーザ5(Own権限あり)権限がある場合を考えます。
    <entry>
      <id>/1/test_own,1</id>
      <link rel="self" href="/1/test_own" />
      <contributor>
        <uri>urn:vte.cx:acl:1,CRUD</uri>
      </contributor>
      <contributor>
        <uri>urn:vte.cx:acl:5,CRUD.</uri>
      </contributor>
    </entry>
    <entry>
      <link rel="self" href="/1/test_own/t001" />
      <title>テストOwn-001</title>
    </entry>
  • ユーザ5が可能な操作は以下となります。
    GET    /1/test_own?e
    PUT    /1/test_own
    DELETE /1/test_own
  • ユーザ5に権限のない操作は以下となります。
    GET    /1/test_own
    GET    /1/test_own/t001?e
    POST   /1/test_own/t005
    POST   /1/test_own/# (自動採番、実際の指定時は#を付けない。)
    PUT    /1/test_own/t001
    DELETE /1/test_own/t001

C4.-(マイナス)スコープの例

  • 以下のようなユーザ1(-権限あり)とユーザ5のaliasがある場合を考えます。
    <entry>
      <id>/1/test_minus,1</id>
      <link rel="self" href="/1/test_minus" />
      <link rel="alternate" href="/5/test_minus" />
      <contributor>
        <uri>urn:vte.cx:acl:-,CRUD</uri>
      </contributor>
    </entry>
    <entry>
      <link rel="self" href="/1/test_minus/t001" />
      <title>テストLow-001</title>
    </entry>
  • ユーザ5が可能な操作は以下となります。
    GET    /5/test_minus?e
    GET    /5/test_minus
    GET    /5/test_minus/t001?e
    POST   /5/test_minus/t005
    POST   /5/test_minus/# (自動採番、実際の指定時は#を付けない。)
    PUT    /5/test_minus
    PUT    /5/test_minus/t001
    DELETE /5/test_minus
    DELETE /5/test_minus/t001
  • ユーザ5に権限のない操作は以下となります。
    GET    /1/test_minus?e
    GET    /1/test_minus
    GET    /1/test_minus/t001?e
    POST   /1/test_minus/t005
    POST   /1/test_minus/# (自動採番、実際の指定時は#を付けない。)
    PUT    /1/test_minus
    PUT    /1/test_minus/t001
    DELETE /1/test_minus
    DELETE /1/test_minus/t001

GR.グループと共有

  • グループを作成することで、複数のユーザーの権限をまとめて管理できます。
  • グループ配下にフォルダを作成することで共有フォルダを作成することができます。
  • 権限追加などでユーザ検索したい場合にグループのメンバーから選ぶことができます。

GR1.グループの作成

  • グループにはシステムグループとユーザグループがあります。
    • システムグループはサービス管理権限(/_group/$admin)などシステムがあらかじめ定義しているグループです。
    • ユーザグループは/{uid}配下にあるグループで(/{uid}/group/{グループ名})ユーザが自由に定義できるグループです。
  • ユーザグループを作るには、グループフォルダ配下に、/{uid}/group/{グループ名} というEntryを作成します。誰でも自由にグループを作成できます。
    • /{uid}/groupをグループフォルダと呼びます。これはユーザ作成時に自動的に登録される特別なフォルダです。
    • uidはグループを作成した本人のものが使われます。
  • 他者のグループEntryにaliasをつけて自分のグループに登録することで、他者が作成したグループに参加することができます。
  • グループEntryには、以下のように、ユーザ自身のOwn(.)CRUD権限と、リンクユーザ(-)のOwn(.)R権限、グループメンバーのLow(/)CRUD権限を指定します。
    • グループ作成者はグループEntryのみ参照可(CRUD.)とします。こうすることでグループ作成者がグループを抜けた場合は配下のデータを見せなくすることができます。
      <entry>
        <contributor>
          <uri>urn:vte.cx:acl:{uid},CRUD.</uri>
        </contributor>
        <contributor>
          <uri>urn:vte.cx:acl:-,R.</uri>
        </contributor>
        <contributor>
          <uri>urn:vte.cx:acl:/{uid}/group/{グループ名},CRUD/</uri>
        </contributor>
        <id>/{uid}/group/{グループ名},{revision}</id>
        <link rel="self" href="/{uid}/group/{グループ名}" />
      </entry>

GR2.グループ権限の設定

  • ACLにGroupKeyを指定することでグループ権限を設定することができます。詳しくは、アクセスコントロール を参照してください。
    <contributor>
      <uri>urn:vte.cx:acl:{GroupKey}</uri>
    </contributor>

GR3.グループメンバーの追加

  • グループにメンバー(以下のユーザB)を追加するには、グループEntryにaliasを設定した後に、追加したメンバーによって署名する必要があります。(詳しくは、署名を参照してください。)
    <entry>
      <id>/{uid}/group/{グループ名},{revision}</id>
      <link rel="self" href="/{uid}/group/{グループ名}" />
      <link rel="alternate" href="/{ユーザBのuid}/group/{グループ名}" />
    </entry>
    • 署名の正しいものだけが有効となりグループに参加しているとみなします。署名がないものはまだ承認されておらず参加していない状態です。
      • ただし、システムグループなどGroupKeyに$が付くもの(例:/_group/$xxxx)は署名がなくても有効です。
    • ユーザ自身は署名しなくてもグループメンバーとなります。
    • グループ参加前の未署名メンバーでもグループEntryだけは参照できます。aliasを指定することでそれ自体は参照可能になりますが配下はまだ見れません。

GR4.メンバーによる承認

  • メンバーを追加するだけではまだグループに参加していません。
  • グループに署名が付けられることで承認され、グループ権限として有効になります。
  • グループに署名を付けるには、_signatureオプションを指定します。
    PUT /{ユーザのグループKey}?_signature={Revision}
  • 上記の例では、ユーザBによって、PUT /d/ユーザBのuid/group/グループ名?_signature=リビジョン のように実行してください。リビジョンには数字が入ります。
  • グループから退会するには、署名を削除します。
    DELETE /{ユーザのグループKey}?_signature
  • グループ作成者がグループ内のユーザを退会させるには、aliasを削除します。
  • グループを削除するには、グループEntryを削除します。
    • 削除権限のあるユーザのみ可能です。

GR5.グループ共有フォルダ

  • グループ配下にEntryを作成することでグループ内で情報共有ができるフォルダを作れます。
  • 例) g001グループの共有フォルダshare
    <entry>
        <id>/1/group/g001/share,1</id>
        <link rel="self" href="/1/group/g001/share" />
    </entry>

GR6.グループ共有フォルダの例

  • 以下のグループentryを登録します。
     <entry>
        <contributor>
          <uri>urn:vte.cx:acl:-,R.</uri>
        </contributor>
        <contributor>
          <uri>urn:vte.cx:acl:/{グループ作成者uid}/group/{グループ名},CRUD/</uri>
        </contributor>
        <contributor>
          <uri>urn:vte.cx:acl:{グループ作成者uid},CRUD.</uri>
        </contributor>
        <id>/{グループ作成者uid}/group/{グループ名},1</id>
        <link rel="self" href="/{グループ作成者uid}/group/{グループ名}" />
        <link rel="alternate" href="/{共有者1uid}/group/{グループ名}" />
        <link rel="alternate" href="/{共有者2uid}/group/{グループ名}" />
        ....
      </entry>
  • 任意のフォルダEntry(/フォルダ作成者uid/folders/フォルダKey)を作り、上記のグループentryのACLとaliasを追加します。このフォルダはグループ間で共有できます。
     <entry>
        <contributor>
          <uri>urn:vte.cx:acl:/{グループ作成者uid}/group/{グループ名},CRUD</uri>
        </contributor>
        <id>/{フォルダ作成者uid}/folders/{フォルダKey},1</id>
        <link href="/{フォルダ作成者uid}/folders/{フォルダKey}" rel="self"/>
        <link href="/{グループ作成者uid}/group/{グループ名}/share/{フォルダKey}" rel="alternate"/>
      </entry>
  • 以下は複数グループで共有できるフォルダになります。
    <entry>
        <contributor>
          <uri>urn:vte.cx:acl:/{グループ1作成者uid}/group/{グループ1名},CRUD</uri>
        </contributor>
        <contributor>
          <uri>urn:vte.cx:acl:/{グループ2作成者uid}/group/{グループ2名},CRUD</uri>
        </contributor>
        <contributor>
          <uri>urn:vte.cx:acl:/{グループ3作成者uid}/group/{グループ3名},CRUD</uri>
        </contributor>
        <id>/{フォルダ作成者uid}/folders/{フォルダKey},1</id>
        <link href="/{フォルダ作成者uid}/folders/{フォルダKey}" rel="self"/>
        <link href="/{グループ1作成者uid}/group/{グループ1名}/share/{フォルダKey}" rel="alternate"/>
        <link href="/{グループ2作成者uid}/group/{グループ2名}/share/{フォルダKey}" rel="alternate"/>
        <link href="/{グループ3作成者uid}/group/{グループ3名}/share/{フォルダKey}" rel="alternate"/>
        ....
      </entry>

A.ユーザ管理

  • ここではユーザ管理について説明します。

A1.メールアドレス

  • メールアドレスの表記は valid e-mail addressに従います。
  • メールアドレスは1つで複数の受信が可能となるような記述方法があり一意にはできません。
    • Gmailでは+以降@までを無視して送信しますが受信では無視しません。例えば、foo+1@gmail.comはfoo@gmail.comで送信し、foo+1@gmail.comで受信します。

A2.ユーザアカウント

  • メールアドレスとユーザアカウントは区別しています。
  • ユーザアカウントは、foo@bar.com のように(すべて小文字の)メールアドレスの形式をとりますが、システムで一意に識別できなければなりません。
  • システム内部的には入力されたメールアドレスを変換(使用不可文字を削除)することで一意なユーザアカウントとして使用しています。
  • ユーザアカウントには英数字、ハイフン(-)、アンダースコア(_)、@、$、ドット(.)が使用可能です。(その他の文字は使用不可)
  • ユーザアカウントはuid(ユーザ識別番号)と関連づけられます。uidはシステムで自動的に振られる一意の連番です。
    0 : システムユーザ (システム内でのみ使用)
    1 : スーパーユーザ
    上記以外の番号を一般ユーザに振り分ける
  • ユーザアカウントはサービスに登録されることによって使用可能になります。
  • ログインする際に求められるパスワードは「8文字以上、かつ数字・英字・記号を最低1文字含む」である必要があります。

A3.グループキー(GroupKey)

  • GroupKeyはユーザアカウントを束ねるグループのKeyです。/1/group/g101 のように、/から始まる英数字の形式をとります。
  • /GroupKeyには英数字、ハイフン(-)、アンダースコア(_)、@、$、ドット(.)が使用可能です。(その他の文字は使用不可)

A4.uid検索

  • 以下によりユーザアカウントからuidを検索します。
  • サービスにログインできれば誰でも検索できます。(アカウント情報が参照できるわけではないため誰でも検索可能にしています)
    GET ?_uid={ユーザアカウント}
  • 戻り値にはfeedのtitleにuidがセットされます。指定されたユーザアカウントがサービスを利用していない場合は-1を返却します。
現在ログインしているアカウントのuidを取得する
  • 以下によりログイン中のuidを返却します。ログインしていない場合はHTTPステータスコード401を返却します。
    GET ?_uid
  • レスポンスヘッダに「X-UID: {uid}」をセットします。
  • レスポンスデータに、<title>にuidを設定したfeedを返却します。

A5.ユーザ情報取得(whoami)

  • 以下によりログイン中のユーザ情報を返却します。ログインしていない場合、HTTPステータスコード401を返却します。
    GET ?_whoami
  • レスポンスデータに、/{uid} エントリーを返却します。これにユーザアカウントやニックネームなどのユーザ情報が含まれます。
  • レスポンスヘッダに「X-UID: {uid}」をセットします。
  • サービスのユーザエントリー(/{uid} エントリー)
    • titleにメールアドレス
    • subtitleにニックネーム
    • summaryにユーザステータス (ユーザステータスの管理を参照)
    • その他項目に、ユーザ情報として保持したい情報

A6.認証チェックとRXID取得

  • 以下により認証チェックを行い、ログイン中のアカウントのRXIDを返却します。ログインしていない場合、HTTPステータスコード401を返却します。
    GET ?_authcheck
  • RXIDを発行し、レスポンスヘッダに「X-RXID: RXID」をセットします。
  • レスポンスヘッダに「X-UID: uid」をセットします。
(補足)ユーザ情報の「公開・非公開」設定について
  • ユーザ登録のサーバサイドJavaScriptにより、/{uid}エントリの読込権限(+,R)を付けることができます。
  • グループごとの設定については以下のように要件に応じてアプリ側で検討すべきこととします。
    • グループ登録(参加)のタイミングで/{uid}エントリに参照権限グループ,Rを付けます。(ただし、公開可能なグループのみ設定すべきでしょう)
    • グループ削除(退会)のタイミングで/{uid}エントリから参照権限グループ,Rを外します。

A7.サービス作成者

  • サービス作成者はサービスを作成するときに使われるアカウントです。作成時に以下の権限が付きます。
    • サービス管理権限(/_group/$admin)
    • ユーザ管理権限(/_group/$useradmin)
    • コンテンツ管理権限(/_group/$content)
サービス代表ノードとデータの格納場所について
  • サービスに振り分けられたノードのうち、サービス作成者の担当ノードが存在します。これを、サービス代表ノードと呼んでいます。
    • 通常、サービス代表ノードはサービスノードのうち一番小さな番号のノードになります。
  • /{uid}フォルダ配下に置かないものは全てサービス代表ノードにデータが格納されます。

A8.サービス管理権限

  • /_group/$admin グループに属しているユーザはサービスの管理権限をもちます。
    • /{uid}/group/admin が参照できればグループに属しています。($のつくグループには署名は不要です。)
  • このユーザは、<contributor>タグおよび、<rights>タグの内容を自由に編集できます。
    • ACLやindexなどの設定をするのに必要な権限です。
  • /_settings にはこのグループのACLが付いており配下の情報などサービス固有の情報にアクセスできます。
  • その他、サービス管理者のみアクセスしたいデータにはACLに /_group/$admin グループを設定してください。

A9.ユーザ管理権限

  • /_group/$useradmin グループに属しているユーザはユーザの管理権限をもちます。
    • つまり、このユーザは、ユーザの追加・削除、ユーザステータスの参照・更新やユーザのサービス内データ削除が自由にできるようになります。
  • ユーザ管理権限を与えてしまうとユーザステータスの参照・更新やユーザのサービス内データ削除が自由にできるようになります。
    • 管理者のみ権限を与えるようにしてください。

A10.コンテンツ管理権限

  • /_group/$content グループに属しているユーザは内部コンテンツの管理権限をもちます。
    • つまり、このユーザは、<content>タグのテキストノードの内容を自由に更新できます。
  • コンテンツ管理権限を与えてしまうとHTMLやJavaScript等のページを自由に作成できるようになります。
    • サービス管理者かアプリケーション開発者にのみ権限を与えるようにしてください。

A11.ユーザステータスの管理

  • サービスごとにユーザの状態(ステータス)を管理します。
  • /@サービス名/{uid} エントリーの <summary> にユーザステータスをセットします。
  • ユーザステータスには以下の種類があります。
    • 登録なし: Nothing
    • 仮登録:Interim
    • 本登録:Activated
    • 無効:Revoked

A12.ユーザステータスの参照

  • ユーザ管理権限(/_group/$useradmin)をもつユーザのみ操作が可能です。
  • GET ?_userstatus={ユーザアカウント} で、指定されたユーザのステータスを取得します。
  • GET ?_userstatus (値なし)で、以下のようなユーザのステータス一覧を取得します。ただし、一定件数を超える場合は feed の <link rel="next"> 要素の href 属性にカーソルを返却します。
    <feed>
      <entry>
        <link href="/{uid}" rel="self"/>
        <title>{ユーザアカウント}</title>
        <subtitle>{ニックネーム}</subtitle>
        <summary>{ユーザステータス}</summary>
      </entry>
      
      ...
      
    </feed>

A13.ユーザステータスの更新

  • ユーザ管理権限をもつユーザのみ操作が可能です。
  • PUT ?_revokeuser={ユーザアカウント} で、指定されたユーザのステータスを「Revoked」に更新します。
  • PUT ?_revokeuser (値なし)で、リクエストデータに以下の内容を設定すると、指定されたユーザのステータスを「Revoked」に一括更新します。uidかユーザアカウントのいずれかを指定できます。
    <feed>
      <entry>
        <link href="/{uid}" rel="self"/>
        <title>{ユーザアカウント}</title>
      </entry>
      
      ...
      
    </feed>
  • PUT ?_activateuser={ユーザアカウント} で、指定されたユーザのステータスを「Activated」に更新します。
  • PUT ?_activateuser (値なし)で、リクエストデータに上記の内容(_revokeuserと同じ)を設定すると、指定されたユーザのステータスを「Activated」に一括更新します。

A14.ログイン

  • ?_loginパラメータが指定された場合、ログイン認証を実行してセッションを開始します。
    • X-WSSEヘッダをセットしてリクエストします。(WSSEの詳細については説明しません。login.htmlのロジックをそのまま利用してください。)
  • 2回ログイン認証に失敗するとCaptcha認証が必要になります。
  • ?_RXIDパラメータが指定された場合もログイン認証を実行してセッションを開始します。
  • サーバサイドJavaScriptのlogin.jsが存在すれば実行します。
    • initlogin.jsは初回ログイン時のみ実行します。

A15.ログアウト

  • ?_logoutパラメータが指定された場合、セッションを破棄します。HTTPステータスコード200を返却します。
  • サーバサイドJavaScriptのlogout.jsが存在すれば実行します。

A16.ユーザ仮登録

  • ?_adduserパラメータをつけて以下をPOSTすることで新規ユーザの仮登録を実行します。
    • ユーザアカウント(メールアドレス)、パスワードをEntryに設定します。
  • /_settings/adduserのcontentタグに記述された文章がメール送信されます。
    • ワンタイムトークン(RXID)が付いたリンクが本文に含まれます。
    • ユーザがこのリンクをクリックするまでは仮登録となります。
  • サービス管理設定でadduserの機能を有効にすることで誰でも実行できるようになります。
    <feed>
      <entry>
        <contributor>
          <uri>urn:vte.cx:auth:{ユーザアカウント},{パスワード}</uri>
          <name>{ニックネーム}</name>
        </contributor>
      </entry>   
    </feed>
  • adduser.jsを登録しておくことでユーザ仮登録実行時にこのJavaScriptを実行させることができます。
  • 生成されたユーザエントリ(/{uid})のACLは以下のようにサービス管理者とユーザ自身のCRUDになります。
    /_group/$admin,CRUD
    {uid},CRUD
  • ACLに例えば、+,R を追加したい場合には、adduser.js 内で /uid エントリーのACLを変更してください。

A17.パスワードリセット

  • ?_passresetパラメータを付けて以下をPOSTすることでパスワードリセットします。
  • 以下のように、ユーザアカウントをリクエストデータに指定します。
    <feed>
      <entry>
        <contributor>
          <uri>urn:vte.cx:auth:{ユーザアカウント}</uri>
        </contributor>
      </entry>
    </feed>
  • /_settings/passresetのcontentタグに記述された文章がメール送信されます。
    • ワンタイムトークン(RXID)が付いたリンクが本文に含まれます。
    • ユーザがこのリンクをクリックすることでパスワードを再設定します。
  • サービス管理設定でパスワードリセット可能にすることで誰でも実行できるようになります。

A18.パスワード変更

  • ?_changephashパラメータを付けて以下をPUTすることでパスワードを変更します。
  • ログイン中のアカウントに対して実行します。
    <feed>
      <entry>
        <contributor>
          <uri>urn:vte.cx:auth:,{パスワード}</uri>
        </contributor>
      </entry>
    </feed>

A19.管理者によるユーザ登録

  • ?_adduserByAdminパラメータを付けて以下をPOSTまたはPUTすることで新規ユーザを登録します。
    • ユーザアカウント(メールアドレス)、パスワードを1件目のEntryに設定します。
    • 2件目以降のEntryを指定することでユーザに関する情報を登録できます。(任意)
      • ユーザ情報の"#"部分には、仮登録時に発行されるuidで置換します。
      • 既に同じユーザが登録されていた場合、POSTを実行するとユーザ登録に失敗してエラーを返しますが、PUTを実行するとエラーとはならずに2件目以降の処理も実行します。
  • ユーザ管理権限(/_group/$useradmin)を持つアカウントのみ実行できます。
  • 本人認証(メールリンクをクリック)しなくても本登録になります。
  • サーバサイドJavaScriptを実行できます。例えば、/s/foo?_adduserByAdmin をリクエストすると、foo.jsに2件目以降のentryが渡されます。戻り値はJavaScriptの実行結果が返ります。
    <feed>
      <entry>
        <contributor>
          <uri>urn:vte.cx:auth:{ユーザアカウント},{パスワード}</uri>
          <name>{ニックネーム}</name>
        </contributor>
      </entry>
      <entry>
        <link rel="self" href="/_group/$admin" />
        <link rel="alternate" href="/#/group/$admin" />  <!-- 管理者権限の付与 -->
      </entry>
      <entry>
        <link rel="self" href="/#/message" />    <!-- ユーザ登録情報 -->
      </entry>
      <entry>
        <link rel="self" href="/#/message/1" />
        <link rel="via" type="email" />           <!-- メールの送信 -->
        <title>ユーザ登録</title>
        <subtitle>foo@bar.com</subtitle>
        <summary>ユーザ登録されました</summary>
      </entry>  
      
      ...
      
    </feed>
  • 生成されたユーザエントリ(/{uid})のACLは以下のようにサービス管理者とユーザ自身のCRUDになります。
    /_group/$admin,CRUD
    {uid},CRUD
  • ACLに例えば、+,R を追加したい場合、以下のように実現できます。
    • adduserByAdminを実行する際のentryの2件目以降に、Keyが/# のエントリーを設定しておき、そのエントリー内にACLの設定をする。
    • ただし、adduserByAdminを実行するユーザが$useradmin権限を持つが$admin権限を持たない場合にはACLの設定ができません。

Z.セキュリティの考慮

  • ここではCSRF対策やDoS(リピート)攻撃対策などのセキュリティ対策について説明します。

Z1.CSRF対策

  • CSRF対策のため、XMLHttpRequestが前提になっています。CookieやhiddenによるCSRFトークンは使用していません。
  • リクエストヘッダに「X-Requested-With: XMLHttpRequest」が付与されていなければエラーを返却します。(ステータス417)
    • このヘッダが付与されていないPOST、PUTリクエストおよび、GETリクエストでJSONを返すものがエラーになります。
    • formのsubmitによるリクエストは受け付けられません。
    • Hostヘッダのチェック、Originヘッダのチェックを行っています。
  • 詳しくは、XMLHttpRequestを使ったCSRF対策 を参照してください。

Z2.DoS(リピート)攻撃対策

  • リピート攻撃に対応するため、ログイン時にはCaptchaを利用しています。(2回以上ログイン失敗でCaptchが要求されます)
  • ブラックリストに登録することでDoS攻撃などの可能性があるリクエストを拒否できます。
    • ブラックリストに登録されているユーザ・IPアドレスの組み合わせからのリクエストは認証エラーとなります。
    • 特定のIPから1000回(デフォルト)以上ログイン失敗でユーザ・IPアドレスの組み合わがブラックリストに自動的に追加されます。
    • ブラックリストに追加されても異なるIPからログインすることは可能です。
  • 認証失敗カウンタキーは、/_security/{アカウント認識文字列}/{IPアドレス}のカウンタが使われます。
    • アカウント認識文字列は、WSSEやRXIDの場合はアカウント (ユーザ名から使用不可文字を除去した文字列)が、AccessTokenやLinkTokenの場合はuidが使われます。
    • IPアドレスは"."を"-"に置き換えた文字列になります。
      • 例) 「125.30.16.141」の場合、"125-30-16-141"。
  • 上記キーのカウンタを0にすることでロックが解除されます。具体的には以下のようなリクエストです。
    • PUT /d/_security/{アカウント認識文字列}/{IPアドレス}?_setids=0

K.認証キーとトークン

  • ここでは認証キーとトークンについて説明します。

K1.Accesstoken

  • Accesstokenは時間制限なしの認証トークンです。curlコマンドやCircleCIなどで利用する際の認証に使うほか、AndroidやiPhoneなどのスマホの認証でも使用します。
  • GET ?_accesstoken で取得できます。
    <feed>
      <title>{Accesstoken}</title>
    </feed>
  • 以下のように、Accesskeyをリクエストヘッダに付与することで認証されます。
    Authorization : Token {Accesstoken}
  • 認証が成功するとログイン状態になります。

K2.Linktoken

  • Linktokenは時間制限なしの認証トークンですが、アクセスできるURLに制限が付けられています。メールに含まれるURLリンクとして使うことを想定しています。
  • GET ?_linktoken={Key1[,Key2],・・} で取得できます。
    • KeyはURLのPathinfoに相当します。
    • #はuidに変換されます。
      <feed>
        <title>{Linktoken}</title>
      </feed>
  • リクエスト時のKeyと完全一致(配下も含む)でチェックされます。Keyを /foo と指定した場合、
    • /foo をエントリー検索、フィード検索することができます。
    • /foo 配下に自動採番でPOSTすることができます。
    • /foo エントリーをPOST、PUT、DELETEすることができます。
    • /foo/bar エントリーのGET、POST、PUT、DELETEはできません。
  • カンマで区切ることでKeyを複数指定できます。
    • 表示したHTMLでさらに別のデータにアクセスする必要がある場合に複数のKeyが必要になります。
    • Linktokenは表示したHTMLのJavaScriptによってURLパラメータから取得します。
  • LinktokenをURLパラメータに含めることで認証されます。
  • また、送信するメールの本文に${LINK=...}を挿入することで、Linktokenを含むURLが自動的に組み立てられます。
    挿入文: ${LINK=/setpass.html}&value=abc
    実際のメール本文: https://test.vte.cx/setpass.html?_token=xxx&value=abc
  • RXIDと異なりログイン状態にはなりません。

K3.Accesskey

  • Accesskeyは、AccesstokenやLinktokenを発行するためのシークレットキーです。サーバで保持しています。
  • PUT ?_accesskey でアクセスキーを更新します。
    • 更新すると、/{uid}/_auth/accesskey エントリーに現在のキー情報を登録します。
    • これまで使っていたAccesstokenやLinktokenは使用できなくなります。

K4.Platformトークン

  • PlatformトークンはMobile Push通知に必要な情報です。
  • 同じユーザが1つのプラットフォームを複数使用することも想定しています。
  • 端末のプラットフォームとregistrationId (iPhoneだとdeviceToken)をリクエストヘッダに設定します。
    X-PLATFORM : 端末のプラットフォーム
        Androidの場合、"gcm"
        iPhoneの場合、"apns"
        Kindleの場合、"adm"
    X-PLATFORM-TOKEN : platformToken
        Android、Kindleの場合、registrationId
        iPhoneの場合、deviceToken
  • /{uid}/_auth/platform/{自動採番} に保存されます。

K5.APIKey

  • APIKeyはワンタイムトークン(RXID)などに使用されるクライアントシークレットキーです。
  • システム管理サービスは各サービスに対してAPIKeyを1つ発行します。
  • APIKeyはサービスEntry(/@{サービス名})に対する署名値になります。(以下のlink title)
    <entry>
        <contributor>
          <uri>urn:vte.cx:acl:{サービス管理者},CRD.</uri>
        </contributor>
        <contributor>
          <uri>urn:vte.cx:acl:{サービス管理者},CRUD/</uri>
        </contributor>
        <contributor>
          <uri>urn:vte.cx:acl:{システム管理者(0)},RUD.</uri>
        </contributor>
        <contributor>
          <uri>urn:vte.cx:acl:+,R/</uri>
        </contributor>
        <id>/@{サービス名},{Revision}</id>
        <link rel="self" href="/@{サービス名}" title="{APIKey(署名)}" />
        <title>{サービス検索名}</title>
        <subtitle>{サービスの状態}</subtitle>
      </entry>
APIKeyの更新
  • APIKeyを更新したい場合は署名の更新リクエストを送信します。
    PUT /@{サービス名}?_signature={Revision}
  • 署名ではシステム管理サービスのusersecretが使用されます。
  • APIKeyを更新するとこれを保持している全てのクライアントから認証ができなくなります。(発行した新しいAPIKeyを配布する必要があります)
    • APIKeyを更新してもAccesstokenには影響はありません。Accesstokenを無効にしたい場合はAccesskeyを更新してください。

K6.ワンタイムトークン(RXID)

  • RXIDは鍵付きハッシュを利用した認証トークンです。ワンタイムであり一度実行すると同じものは使えません。
  • AndroidやiPhoneなどのスマホのログイン認証で使われます。
    • トークン生成にはユーザアカウント、パスワード、APIKey、サービス名を必要としますが、ハッシュ化された文字列なのでネットワーク上で生のパスワードを流さなくて済みます。
  • また、送信するメールの本文に${RXID=Key}を挿入することで、RXIDを含むURLが自動的に組み立てられます。
    挿入文: ${RXID=/setpass.html}&value=abc
    実際のメール本文: https://test.vte.cx/setpass.html?_RXID=xxx&value=abc
  • このURLをクリックするとRXIDに含まれるアカウントでログインしたうえで表示されます。(以降、ログイン済となります)
    • リンクに有効時間を設定することができます。
      • _rxid.minute : 有効時間(分)。[デフォルト15]
    • クリックできる回数を設定することができます。
      • _rxid.counter.連番.回数 : 同じRXIDを使用しても指定回数まで許可されるURLパターンを指定
    • /_settings/propertiesのrightsタグに設定します。
      _rxid.minute=60
      _rxid.counter.1.10000=^/_html/foo.html.*$
  • リクエストのHTTPヘッダに以下をセットすることで認証します。
    Authorization: RXID {RXIDトークン}
  • URLパラメータに?_RXID={RXIDトークン} と付けることもできます。
トークンの形式
  • RXIDは以下の形式をしています。
    {Createdのタイムスタンプ13桁}-{Nonce}-{PasswordDigest}-{usernameのrotate13文字列}
    
    例) 20131115144337P09-JYdRiPmQ7JD-t3Lkktx12VFu9nA8QMVK9JHCgE1xaMgATbagXkzQf4Z-ybtvafhcre
  • Createdはシステム時間と比較して5分より未来のものは受け付けられません。また、設定情報で指定された有効時間より過去の場合もエラーとなります。
  • APIKeyはサービスごとに発行される秘密のKey(鍵)です。
  • Passwordには実際のパスワードをsha256ハッシュ化してBase64文字列に変換したものになります。
  • usernameはユーザアカウントとサービス名を:でつなげた値です。

G.GET(リソース取得)

  • クライアントからURLおよびURLパラメータのGETリクエストを受け付けます。
  • URLパラメータの予約語は1文字の英字、もしくは先頭_(アンダースコア)で始まる文字列になります。これ以外のもの、つまり2文字以上で_で始まらない文字列についてはユーザアプリケーションのパラメータとして自由に使えます。

G1.Entry取得

  • ?eパラメータを指定することで、指定されたKeyのEntry検索を行い、Feed内にEntryを1件設定し返却します。
  • エラーの場合はFeedタグにエラーのステータスコードとメッセージを入れて返します。
  • エントリーが存在しない場合、HTTPステータスコード204を返却します。レスポンスデータは空になります。
  • ReflexContext#getEntryに対応します。

G2.Feed取得

  • ?fパラメータを指定することで、指定されたKey配下のフィード検索を行い、Feed内にEntryをすべてセットして返却します。
  • 次ページ検索のためのカーソルがある場合、feed.linkタグのrel="next"属性のhref属性にセットします。
  • エラーの場合はFeed.titleタグにメッセージを入れて返します。
  • エントリーが存在しない場合、HTTPステータスコード204を返却します。レスポンスデータは空になります。
  • フィード内Entryの最大件数はlパラメータで指定します。
  • ReflexContext#getFeedに対応します。

G3.条件検索

  • URLにKeyを指定すると配下の一覧が取得できます。
    • 例) 引数に"/Tops"と指定すると、"/Tops/Jacket"、"/Tops/Sweater" など配下のデータが返ります。
  • また、URLパラメータに絞り込み条件を複数指定することができます。
  • ReflexContext#getFeedに対応します。
  • フェッチの最大実行回数を超える場合は206(Partial Content)となります。詳しくは、fetch limitと対応方法 を参照してください。
条件指定方法
<文法>
http://xxx.xxx/{Key}?f&{name}{=|-eq-|-lt-|-le-|-gt-|-ge-|-ne-|-rg-}{value}&{name}{=|-eq-|-lt-|-le-|-gt-|-ge-|-ne-|-rg-}{value}&...&l={n}&p={カーソル文字列}

<例:好きな食べ物がeggで価格が5000以下の最大件数20件を取得>
http://xxx.xxx/{Key}?f&subInfo.favorite.food-eq-egg&price-lt-5000&l=20
  • 「項目名=値」の形で条件を指定できます。項目名には、テンプレートの階層を"."でつないだ名称を指定します。
  • 等式以外を指定したい場合は「項目名-記号-値」の形で条件を指定できます。
  • 記号の種類は以下の通りです。
    • eq : = (等しい)
    • lt : < (未満)
    • le : <= (以下)
    • gt : > (より大きい)
    • ge : >= (以上)
    • ne : != (等しくない)
    • rg : regex (正規表現に合致する)
  • 配列項目についても検索できます。配列の要素のうちどれか一つでも合致すれば条件を満たします。
URLエンコードについて
  • ブラウザから直接URLの指定ができるようにURLエンコードする項目は最小限にしています。 URLパラメータのうち、URLエンコードを実行しているのは以下の項目のみです。(ReflexContextにおけるパラメータも同様)
    • pathInfo(クエリーパラメータを除くパスの部分)
    • p (カーソル)
Index検索
  • 検索条件の最初の項目がIndex指定されている場合にIndex検索を行います。Indexはテンプレートで指定できます。詳しくは、「 Index、暗号化、項目ACL用テンプレート」を参照してください。
  • まずIndex検索して絞り込みを行い、2つ目以降の検索条件はインメモリ検索となります。
  • 例えば、/foo/bar?title=a*&subtitle=b であれば、/foo/barフォルダに属し、かつ、title=a*の前方一致(Index)検索によって絞り込みを行い、次にその中からインメモリでsubtitle=bに合致するものを検索します。
  • 1つ目の条件は完全一致か前方一致である必要があり、また暗号化項目は条件指定できません。2つ目以降の条件はあいまい検索や暗号化項目の検索が可能です。
前方一致検索
  • プロパティ指定で等式を指定し、末尾に*を指定することで前方一致検索ができます。
  • また上位階層についても、末尾に*を指定することで、階層の前方一致検索ができます。
    • 例) 引数に"/Men/To*"と指定すると、"/Men/Tokyo"、"/Men/Tops" などが返ります。
  • クエリーパラメータの条件値の末尾に*を付けると前方一致検索となりますが、"*"が検索文字である場合には、URLエンコードした値(%2A)に変換する必要があります。
あいまい検索
  • プロパティ指定で等式を指定し、先頭および末尾に*を指定することであいまい検索ができます。ただし、2つ目以降の絞り込み条件のみ、つまりインメモリ検索においてのみ使えます。
取得件数指定とカーソルの返却
  • 「l=数値」を指定することで、指定された件数までのEntryを結果に返します。
  • 続きのデータが存在する場合、Feedのlink rel=nextタグのhref属性にカーソルが自動的に設定されます。
カーソル指定
  • 「p=カーソル」を指定することで、続きのデータを取得できます。
nocontent
  • _nocontentを指定することで、結果のEntryについて、contentタグの中身を空にして返します。
  • エラーの場合はFeedタグにエラーメッセージを入れて返します。

G4.getMulti

  • POST ?_getmulti により、複数のKeyで検索し、結果をフィードに詰めて返却します。
  • リクエストデータは、「{Key},{Key}, ... 」と、Keyをカンマ区切りにして渡します。(Entryタグなどで括る必要はありません)

G5.ソート

  • 条件指定なしで検索を行うとKeyの昇順にソートします。例えば、プロパティ指定で等式を指定し、最初の条件指定で、「項目=*」を付ける(例えば、?title=*)と、その項目の昇順で検索ができます。
    • ただし、Index指定されている必要があります。(インデックス検索を行うとインデックスの値の昇順にソートします) 検索パラメータは先頭に指定してください。(2番目以降だとインメモリ検索となってソートが働きません)
      ?{項目}=*&・・
  • 降順にソートを行いたい場合、項目とは別に降順用の項目(desc型)を追加することで実現できます。desc型は以下のようにLongの最大値から引いた値となります。条件指定でこの項目を指定すると降順となります。
    • desc型には、Long.MAX_VALUE - 降順ソートしたい項目の値が代入されます。(テンプレートによるスキーマ定義>A.記述ルールを参照)
    • desc型は降順ソートを目的としたもので、実際の値を取得することはできません。GETしてもEntryの中に項目は現れません
    • Index指定されている必要があります。例えば、/log配下のデータを項目createdで降順にしたい場合、以下をtemplateのrightsに記述します。GET /log?created_desc=*で取得すると降順になります。
      created_desc:/log

G6.件数取得

  • ?cパラメータを指定することで、指定した階層直下のデータの件数が取得できます。
  • 条件検索と同じく、プロパティ指定や前方一致検索が指定できます。
  • 戻り値はFeed形式で、titleに件数を設定します。
  • フェッチの最大実行回数を超える場合は206(Partial Content)となります。詳しくは、fetch limitと対応方法 を参照してください。
  • エラーの場合はFeed.titleタグにエラーメッセージを入れて返します。
  • ReflexContext#getCountに対応します。

G7.ページネーション

カーソル一覧(pageindex)作成
  • 指定されたページ数分のpageindexを取得し、セッションに保持します。
    • GET {Key}?_pagination={最終ページ番号}
    • GET {Key}?_pagination={開始ページ番号},{最終ページ番号}
  • l={件数}パラメータを指定することで、1ページあたりの件数を指定できます。
  • サーバ内で非同期に実行します。
  • pageindexに、指定された最終ページ番号より大きいページ番号がある場合は削除します。
  • 開始ページ数指定の場合、セッションに「開始ページ数-1ページ」のカーソルが登録されていなければエラーになります。
  • フェッチの最大実行回数を超える場合は206(Partial Content)となります。詳しくは、fetch limitと対応方法 を参照してください。
    • ただし、検索条件がindex検索になっている場合はフェッチの最大実行回数を超えることはありません。
ページ数指定検索
  • 指定されたページ数のデータを取得します。
  • GET {Key}?{検索条件}&n={ページ番号}
  • l={件数}パラメータを指定することで、1ページあたりの件数を指定できます。
  • セッションに指定条件のpageindexが存在しない場合は400エラーとなります。また、feed.titleに、「Please make a pagination index in advance. 」を入れて返します。
    • pageindexは生成に若干時間がかかる場合があります。pageindexがまだ生成されていない状態でページを表示しようとするとこのエラーになるため再検索する必要があります。以下のソースコードの「もしまだpageindexが張られていなければ1秒待って再検索する」部分をご覧ください。
paginationのサンプルプログラム(jQuery)
//現在のページ設定
var _activePage;
var _url;
var _feeddata = {};
var _pageindex=0;

var pagination = function(page,param) {

        // pageが取得済のpageindexより大きな値の場合にpage+9までindexを取得する
        var pageindex = page+9>_pageindex ? page+9 : _pageindex;
        if (pageindex>_pageindex) {

                if (_pageindex>1) {
                        param = _pageindex+','+pageindex+param;
                }else {
                        param = pageindex+param;                        
                }
                // 1ページ=50件で開始ページ〜終了ページまでpageindexを張る
                $.getJSON('/d/registration?f&l=50&_pagination='+param, function(json){
                        // 
                }); 
                _pageindex = pageindex;
        }
}

var getFeed = function(param) {

                $.ajax({
          url: '/d/registration?f&l=50'+param,
          method: 'get',
          dataType: 'json'
        }).done(function( res ) {
          if(res) {
                _feeddata.feed = res.feed;
                // 件数が変わっているかもしれないので取り直す
                    $.ajax({
                  url: '/d/registration?c'+param,
                  method: 'get',
                  dataType: 'json'
                }).done(function( res ) {
                  _feeddata.table_count = res.feed.title; // 件数
                  drawtable();
                }).fail(function( jqXHR, textStatus, errorThrown ) {
                        location.href = 'login.html';
                });
          }
        }).fail(function( jqXHR ) {
                // もしまだpageindexが張られていなければ1秒待って再検索する
                if (jqXHR.responseJSON.feed.title=='Please make a pagination index in advance.') {
                        setTimeout(function(){getFeed(param)},1000);    
                }else {
                        location.href = 'login.html';                   
                }
        });
}

// データ取得(引数は取得するページ数と検索条件)
var getTableData = function(url, page,param) {

        // 取得するデータのURL
            _url = url;
            // 取得するページをグローバル変数に保存
            _activePage = page;

                // pageindex取得
                pagination(page,param)

                // 取得するページを指定
                if (page>1) {
                        param = '&n='+page+param;
                }           
            // データと件数取得
                getFeed(param);
                
}

G8.fetch limitと対応方法

  • サーバ負荷軽減のため検索条件検索(含む件数取得)や検索条件付きページネションにおいて一定の件数を検索(fetch)したら一旦終了するようにリミッターを導入しています。
  • 戻り値に検索したところまでの検索結果にカーソルを付与し、ステータス206(Partial Content)を返却します。
  • クライアントが戻り値のカーソルを「p=カーソル」の形でクエリ文字列に付加してリクエストすると、続きの結果が取得できます。

I.採番処理

I1.allocids

  • PUT ?_allocids={採番数}パラメータを指定することで採番処理を行います。
  • 指定された採番数だけ番号を採番します。採番はKeyごとに行います。
  • 採番数にはマイナスの数値を指定することも可能です。
  • GET ?_allocidsもしくは、PUT ?_allocidsで採番数に0が指定された場合、現在の番号を返却します。
  • 採番数に"setting"が指定された場合、現在設定されている採番枠を返却します。
    • 戻り値はFeed形式で、titleに採番された値を設定します。採番された値をすべて返却します。複数の場合はカンマで区切られます。
  • エラーの場合はFeed.titleにエラーメッセージを入れて返します。
  • ReflexContext#allocidsに対応します。

I2.setids

  • PUT ?_setids={設定値}パラメータを指定することで採番番号の設定を行います。
  • 指定された番号を設定します。
  • エラーの場合はFeed.titleにエラーメッセージを入れて返します。
  • ReflexContext#setidsに対応します。

I3.addids

  • PUT ?_addids={加算数}パラメータを指定することで採番番号の加算処理を行います。
  • パラメータで指定した加算数だけ採番の値をプラスして現在値を返します。
  • 加算する数にはマイナスの数値を指定することも可能です。
  • 現在の番号を知りたい場合はGET ?_allocidsを実行してください。
  • 戻り値はFeed形式で、titleに加算後の現在値を設定します。
  • エラーの場合はFeed.titleにエラーメッセージを入れて返します。
  • ReflexContext#addidsに対応します。

I4.rangeids

  • POST ?_rangeids を指定、POSTデータに採番範囲を{start}-{end}の形式で指定します。
  • 接頭辞を指定したい場合、採番範囲の後にカンマと接頭辞を指定します。
  • 採番初期値に設定する値については、以下を参考にしてください。
    • 例1) 初期値を1000とし、2000を超えたら1000に戻る指定 : 1000-2000
    • 例2) 採番範囲を1000から2000とし、先頭に"A"を付けたい場合 : 1000-2000,A
  • 戻り値はFeed形式にメッセージを設定したものです。以下の内容です。
    • title : "Put allocids."
  • エラーの場合はFeed.titleにエラーメッセージを入れて返します。
  • ReflexContext#rangeidsに対応します。

P.POST(リソース登録)

P1.Entry登録

  • Entryを1件登録します。
  • <id>タグは不要です。
  • Keyを指定する場合、link rel=selfタグのhref属性に指定してください。
    • 例) /foo/bar Entryを登録する
      POST /d      <!-- URLは/dだけで構いません -->
      
      <entry>
        ・・・
        <link href="/foo/bar" rel="self"/>     <!-- link rel="href" にKeyを指定します -->
      </entry>
  • link rel=selfタグを省略した場合は自動採番になります。ただし、URLに親階層フォルダを指定する必要があります。(上の例ではPOST /d/foo)
    • 自動採番の値は3-4-x-..というような文字列でありシステム全体でユニークな値になります。つまり、このEntryがどのフォルダに移動したとしても他のEntryと衝突することはありません。
    • 自動採番では内部に複数のカウンタを持っているため同時実行による処理待ちはほとんどありません。
    • 例) /foo/{自動採番}を登録する
      POST /d/foo   <!-- フォルダのURLを指定します -->
      
      <entry>
        ・・・       <!-- link rel=self は設定しません -->
      </entry>
  • Keyには英数字と$、_(アンダースコア)が使えます。
  • 以下のタグにはTagging Serviceが自動で値を代入します。
    • id : 「{Key},{Revision}」を代入
    • author : uri属性に「urn:vte.cx:created:{uid}」を代入
    • updated : 更新日時を「yyyy-MM-dd'T'hh:mm:ss.SSS+99:99」(ISO8601拡張)形式で代入
    • published : 登録日時を「yyyy-MM-dd'T'hh:mm:ss.SSS+99:99」(ISO8601拡張)形式で代入
  • 戻り値はFeedタグにメッセージを入れて返します。
    • HTTPステータスコード : 201
    • title : 登録したKey
  • 上位階層フォルダが存在しない場合、登録できずエラーとなります。
  • エラーの場合はFeed.titleタグにエラーメッセージを入れて返します。
  • CSRF対策のためXHR通信からのみ受け付けるようにしています。
    • リクエストヘッダに「X-Requested-With: XMLHttpRequest」が設定されていなければエラーを返却します。(ステータス417)
  • ReflexContext#postEntryに対応します。

P2.Feed登録(ReflexContext#postFeed)

  • Feed内のEntryをまとめて登録します。すべてのEntryはトランザクションで括られ一貫性が保証されます。
  • 戻り値はFeedタグにメッセージを入れて返します。
    • HTTPステータスコード : 201
    • title : 登録したKey。Entryが複数の場合はカンマでつながれています。
  • 上位階層が存在しない場合、登録できずエラーとなります。
  • エラーの場合はFeed.titleタグにエラーメッセージを入れて返します。

P3.別名(Alias)登録の注意点

  • 別名(Alias)に対して登録するにはEntryの実体を指定する必要があります。グループフォルダへの登録などが想定されます。
  • 例えば、「/1/group/folder」の別名である「/7/group/folder」に対してPOSTしたい場合、以下のように実行します。
    • 実体をidに設定します。自動採番部分は#で指定します。
  • リクエストはURLに指定したKeyでACLチェックが行われます。以下の例では「/7/group/folder」になります。
    POST /7/group/folder
    
    <feed>
      <entry>
        <id>/1/group/folder/#,0</id>
        ....
      </entry>
    </feed>

P4.ファイルアップロード

  • 以下のように、FormDataオブジェクトを使ってファイルを登録することができます。詳しくは、FormData を参照してください。
  • ファイルのentryは指定したPathinfoの下に作成されます。
    • 以下の例では、/entry/{ファイル名}が作られます。
    • エントリのKeyはサーバサイドJavaScriptのReflexContext.saveFiles()を介すことで自由に設定できます。input type="file"のnameをKeyとします。詳しくは、saveFiles を参照してください。
  • 同時に複数のfileオブジェクトを扱えますがtext項目を指定することはできません。
    <!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>ファイルアップロードサンプル</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    </head>
    <body>
    
    <form id="form">
    <p>写真:
    <input type="file" id="file" name="picture"></p>
    <p><input type="submit" id="submitBtn" value="登録する"></p>
    </form>
    
    <script>
    $(function() {
      $('#submitBtn').on('click', function(evt) {
        var form = $('#form').get()[0];
        var formData = new FormData( form );
    
        $.ajax({
          url: '/entry'
          method: 'post',
          dataType: 'json',
          data: formData,
          processData: false,
          contentType: false
    
        }).done(function( res ) {
          // 送信成功
          console.log( 'SUCCESS', res );
        }).fail(function( jqXHR, textStatus, errorThrown ) {
          // 送信失敗
          console.log( 'ERROR', jqXHR, textStatus, errorThrown );
        });
        return false;
      });
    });
    </script>
    </body>
    </html>

U.PUT(リソース更新)

U1.Entry更新

  • Entryを1件更新します。
  • ?_contentパラメータ付きのリクエストについては内部コンテンツとして扱い、<content>タグ内にデータを格納します。
    • 内部コンテンツの場合、リビジョンのチェックは行われず、また元と内容が同じであれば更新しません。
  • Keyをlink rel=selfタグのhref属性に指定します。
  • 楽観的排他チェックを行う場合はidを指定してください。GETで取得したidをそのまま使用してください。+1する必要はありません。
  • idを省略すると強制上書きとなります。また、既存のデータが存在しない場合は登録となります。
  • 第一階層の項目に値が代入されているものを丸ごと置き換えます。
    • 第二階層以下の項目が省略されている場合は空白で上書きになります。
  • 値が代入されていない項目については更新しません。
  • AndroidなどでEntryクラスを使う場合は、記述されているフィールドに値が代入されていれば更新、いなければ更新しないという動きをします。
    • 例えば、ある子要素のクラス内の項目を更新したければ、その項目全ての値を代入した状態でPUTする必要があります。子要素のフィールドが定義されていなければデータは更新しません。
  • ただし下記のタグに関しては以下のように処理します。
    • link : rel属性の単位で設定されているものを置き換えます。
    • contributor : uriタグの"urn:vte.cx:"の次の項目(aclやusersecret)ごとに更新します。
  • 以下のタグには自動で値が代入されます。
    • id : Revisionをインクリメント
      • ただし、URLパラメータに_silentオプションが指定されている場合はインクリメントしません。
    • author : uri属性に「urn:vte.cx:updated:{uid}」を代入
    • updated : 更新日時を「yyyy-MM-dd'T'hh:mm:ss.SSS+99:99」(ISO8601拡張)形式で代入
  • 戻り値はFeedタグにメッセージを入れて返します。
    • title : "Updated."
  • エラーの場合はFeed.titleタグにエラーメッセージを入れて返します。
  • 対象データが存在しない場合は、HTTPステータスコード404を返却し、レスポンスデータを返却します。
  • CSRF対策のためXHR通信からのみ受け付けるようにしています。つまり、リクエストヘッダに「X-Requested-With: XMLHttpRequest」が設定されていなければエラーを返却します。(ステータス417)
  • ReflexContext#putEntryに対応します。

U2.Feed更新

  • Feed内のEntryをまとめて更新します。Entry同士はトランザクションで括られ一貫性が保証されます。
  • 戻り値はFeedタグにメッセージを入れて返します。
    • title : "Updated."
  • エラーの場合はFeed.titleタグにエラーメッセージを入れて返します。
  • 対象データが存在しない場合は、HTTPステータスコード404を返却し、レスポンスデータを返却します。
  • ReflexContext#putFeedに対応します。

D.DELETE(リソース削除)

D1.Entry削除

  • 指定されたKeyのデータを1件削除します。
  • URLパラメータrにidを指定すると楽観的排他チェックを行います。?r={id}
    • r=を省略した場合、楽観的排他は行わず、全てのRevisionを削除します。?r={全てのRevision}
  • 戻り値はステータスコード200で返します。(※ レスポンスデータは返しません)
  • 子エントリーをもっていれば「Can't delete for the child entries exist.」エラーとなります。
  • エラーの場合はFeed.titleタグにエラーメッセージを入れて返します。
  • ReflexContext#deleteEntryに対応します。

D2.Feed削除

  • ?fオプションを指定してDELETEを実行するとFeed削除になります。
  • 指定された上位階層配下のデータをまとめて削除します。
    • 例) 引数に"/Tops"と指定すると、"/Tops/Jacket"、"/Tops/Sweater" を削除します。 "/Tops"は削除されません。
    • 配下のデータに子エントリーをもつものがあれば「Can't delete for the child entries exist.」エラーとなります。
    • 子エントリーを含むすべてのエントリーを削除するには、?_rfオプションをつけて実行してください。
  • 戻り値はステータスコード200で返します。(※ レスポンスデータは返しません)
  • エラーの場合はFeed.titleタグにエラーメッセージを入れて返します。
  • ReflexContext#deleteFeedに対応します。

D3.メソッドオーバーライド

  • クライアントからPUTやDELETEメソッドでリクエストを送信できない場合、またURLに情報を載せたくない場合に使用します。 リクエストヘッダにメソッドや拡張パス情報、URLパラメータを指定できます。
    • Key : X-HTTP-Method-Override
    • 値 : {GET|POST|PUT|DELETE} {拡張パス情報?URLパラメータ}
      • 例) X-HTTP-Method-Override: GET /10001/article?param1=value1&param2=value2
  • リクエスト先が担当ノードでなく、別のノードにリダイレクトされる場合、メソッドオーバーライド情報はCookieのOVERRIDE項目に設定します。

E.データ操作の例

  • 以下のリクエストを実行することにより動作を確認してみます。
    • 検索 : GET
    • 登録 : POST
    • 更新 : PUT
    • 削除 : DELETE

E1.登録:POST

  • 例として、/1/member配下に以下のようなデータをPOSTします。XMLの例ですがJSONも同様に登録できます。
    <feed>
    <entry>
      <member>
        <member_code>00001</member_code>
        <member_name>◯◯ 太郎</member_name>
        <address>東京都港区芝×丁目△-▼</address>
        <phonenumber>03-1111-1111</phonenumber>
        <gender>1</gender>
        <birthdate>2000-01-01</birthdate>
        <profile>テストデータです。</profile>
      </member>
      <link href="/1/member/00001" rel="self"/>
    </entry>
    
    <entry>
      <member>
        <member_code>00002</member_code>
        <member_name>◯◯ 次郎</member_name>
        <address>東京都港区赤坂×丁目△-▼</address>
        <phonenumber>03-2222-2222</phonenumber>
        <gender>1</gender>
        <birthdate>2000-01-02</birthdate>
        <profile>テストデータ2番目です。</profile>
      </member>
      <link href="/1/member/00002" rel="self"/>
    </entry>
    
    <entry>
      <member>
        <member_code>00003</member_code>
        <member_name>◯◯ 三郎</member_name>
        <address>東京都港区芝浦×丁目△-▼</address>
        <phonenumber>03-3333-3333</phonenumber>
        <gender>1</gender>
        <birthdate>2000-01-03</birthdate>
        <profile>テストデータ3番目です。</profile>
      </member>
      <link href="/1/member/00003" rel="self"/>
    </entry>
    </feed>
  • HTTPステータスコード201、feed.titleに登録Keyが入っているEntryが返却されることを確認してください。

E2.検索:GET

  • 例として、/1/member配下のデータを一括取得します。
    • GET http://{ホスト名}/d/1/member?f
    • /1/member配下のデータがFeedで括られたJSON文字列で返却されます。ただし、JSONを取得するにはXHRリクエストである必要があり、リクエストヘッダに「X-Requested-With: XMLHttpRequest」が設定されていなければエラーを返却します。(ステータス417) ブラウザにURLを指定しても表示されません。
  • /1/member配下のデータをXML形式で一括取得します。
    • GET http://{ホスト名}/d/member?f&x
    • /1/member配下のEntryがFeedで括られたXML文字列で返却されます。XMLであればXHRリクエストである必要はありません。(ブラウザにURLを指定することで表示できます)
  • /1/member/00001のEntryをXML形式で1件取得します。
    • GET http://{ホスト名}/d/1/member/00001?x&e
    • /1/member/00001のEntryがXML形式で返却されます。
  • /1/member配下のデータで、電話番号が03-3333-3333のデータを検索して取得します。
    • GET http://{ホスト名}/d/1/member?x&phonenumber=03-3333-3333
    • /1/member/00003のEntryがXML形式で返却されます。
  • /1/member配下のデータの総数を取得します。
    • GET http://{ホスト名}/d/1/member?c
    • titleにデータ数が設定されたFeedが返却されます。

E3.更新:PUT

  • 例として、/1/member/00002のEntryを更新します。
    • GETで取得されたidをEntryに含むことで楽観的排他制御が可能です。idの構成は、「{Key},{Revision番号}」です。
    • 更新は全体が更新されるのではなく部分置換になります。更新対象はEntryの第一階層のタグになります。
      • つまり、Entryタグの第一階層のタグを比較して異なればその内容を入れ替えます。
      • ただし、linkタグは異なる更新ロジックとなります。詳しくは、Entry更新 を参照してください。
  • 以下の例ではmemberタグの内容は全て置き換えられることになります。
  • PUT
    <entry>
      <member>
        <member_code>00002</member_code>
        <member_name>◯◯ 次郎</member_name>
        <gender>1</gender>
        <birthdate>2000-01-02</birthdate>
        <profile>プロフィール更新しました。</profile>
      </member>
      <id>/1/member/00002,1</id>
      <link href="/1/member/00002" rel="self"/>
    </entry>
    • title=Updated.と設定されたFeedが返却されると更新成功です。
    • このEntryをGETすると、profileの値が更新され、addressとphonenumberが削除されていることが確認できます。
  • また、同じEntryに対してmemberを設定せず、titleタグを設定してPUTしてみます。
    • idのRevision番号は一度更新されているためカウントアップされます。1をプラスした値をセットしてください。
      <entry>
        <title>タイトル</title>
        <id>/1/member/00002,2</id>
        <link href="/1/member/00002" rel="self"/>
      </entry>
    • PUT成功後、このEntryをGETすると、memberタグの内容はそのままで、titleタグが追加されていることが確認できます。

E4.削除:DELETE

  • 例として、/1/member/00003のEntryを削除します。
    • URL - http://{ホスト名}/d/1/member/00003?x&r=/1/member/00003,1
    • r={id}を指定することでリビジョンを含む実体を正確に削除することができます。
    • r={数字}でリビジョンを指定して削除ができます。rは省略することもできます。
    • HTTPステータスコード204が返却されることを確認してください。
    • DELETE成功後、/1/member配下をGETすると、/1/member/00003が削除されていることが確認できます。
  • さらに、/1/member配下のすべてのEntryを削除します。
    • URL - http://{ホスト名}/d/1/member?x&_f
    • HTTPステータスコード204が返却されると削除成功です。
    • DELETE成功後、/1/member配下をGETすると、HTTPステータスコード204が返却され、/1/member配下のデータが全て削除されていることが確認できます。

S.署名

  • 書類にハンコを押すように実際の取引においてエビデンスを残したいことがあります。
  • Entryに署名をつけることで承認した人の記録を残すことができます。
  • グループにおけるメンバー承認のプロセスにおいても署名が使われます。
  • selfまたはaliasのlinkに署名を付けることができます。
  <link rel="{self | alternate}" href="{Key}" title="{署名({revision},{文字列})}" />

S1.署名の付与

  • ''PUT {Key}?_signature={Revision}'' で署名付与となります。Revisionは数字になります。
  • 自分自身、つまり、Keyの先頭がログインユーザと一致する場合のみ付与可能です。
  • 既に署名が付いている場合は更新します。
  • 更新の際はRevisionを+1し、更新時刻を新たに設定します。
  • 署名ができるのは、idのKey、またはaliasのKeyのみです。
    • idのKey、aliasのKeyのいずれにも合致しないものは、署名先Keyが存在しないためエラー(No Entry)となります。
戻り値
  • 正常に署名できた場合、HTTPステータスコード200が返ります。
    <feed>
      <title>200</title>
      <subtitle>A signature has been applied.</subtitle>
    </feed>
  • ログインしていない場合、HTTPステータスコード401が返ります。
  • 指定されたKeyに署名する権限がない場合、HTTPステータスコード403が返ります。

S2.署名の削除

  • ''DELETE {Key}?_signature'' で署名削除となります。
  • Keyの先頭がログインユーザの場合のみ削除可能です。
  • 署名削除の際も通常の更新と同じくRevisionを+1し、更新時刻を新たに設定します。
  • linkタグのtitle属性に"-"(署名削除)を設定します。
    • 署名がないものとの区別が必要になるケースがあります。例えば、自分で作成したグループに署名は設定しないため、グループから抜ける場合は署名をブランクにするのでなく、無効な値をセットする必要があります。
戻り値
  • 正常に署名削除できた場合、HTTPステータスコード200が返ります。
    <feed>
      <title>200</title>
      <subtitle>The signature has been truncated.</subtitle>
    </feed>
  • ログインしていない場合、HTTPステータスコード401が返ります。
  • 指定されたKeyに署名する権限がない場合、HTTPステータスコード403が返ります。

S3.署名の検証

  • ''GET {Key}?_signature'' で署名検証となります。
  • 署名検証したいEntryのKeyを指定します。
  • 署名のあるEntryの参照権限があれば署名検証の実行が可能です。
  • 検証に使用するUserSecretは通常のEntryとサービス管理Entryで異なります。
    • サービス管理Entry (/@{サービス名} または /@{サービス名}/_ ...)の場合、サービス管理ユーザのUserSecretで検証します。
    • 通常のEntry(上記以外)の場合、Keyに指定されたサービス名・uidのユーザのUserSecretで検証します。
戻り値
  • 正常に署名検証できた場合、HTTPステータスコード200が返ります。
    <feed>
      <title>200</title>
      <subtitle></subtitle>
    </feed>
  • 署名が不正である場合、''HTTPステータスコード412(The signature is invalid.)''が返ります。
  • 指定されたKeyのEntryそのものを参照する権限がない場合、HTTPステータスコード403が返ります。

S4.署名値の計算方法

  • 以下のように、Entryを特定するKeyとRevision、および、ユーザの秘密鍵(usersecret)と日時を組み合せてハッシュ値を計算します。
  • Entryのpublishedは登録時の値でありPUT更新では変更されません。また、Revision値は更新によって変更されますが、署名付与時に保持しているRevision値を使うため署名検証には影響しません。
    sha256({usersecret} + {Key} + {Revision} + {Entryのpublished})

T.テンプレートによるスキーマ定義

  • テンプレートに項目名などを記述することでスキーマを定義できます。システム運用中に項目の追加が可能であり更新すると直ぐにシステムに反映します。
  • スキーマには型の指定、親子関係や繰り返しといった構造の定義、必須チェックや最大/最小チェック、バリデーション、Index、暗号化などを指定できます。

T1.スキーマ記述ルール

  • 項目名(型){多重度}!=正規表現 の形式で項目を記述します。
  • 項目名は2文字以上128文字以下の英数字および一部の記号(_や$)が使えます。ただし、数字で始まるものやハイフンは使用不可です。
  • インデントが下がると子要素であることを示します。
  • ()の中には型を指定します。型には、string,int,date,long,float,double,boolean,descがあります。
    • ()を省略した場合や前述した型以外が指定された場合はStringになります。型名は先頭は大文字小文字のどちらでも構いません。(case sensitiveではない)
  • String型に格納できる文字の最大サイズは64KiBです。これを超えるラージオブジェクトについては、EntryのContentタグに格納してください。
  • Date型は以下の形式のものを受け付けます。
    yyyy-MM-dd
    yyyy-MM-dd HH
    yyyy-MM-dd HH:mm
    yyyy-MM-dd HH:mm:ss
    yyyy-MM-dd HH:mm:ss.SSS
    上記の"-"を"/"にしたもの
    上記の" "を"T"にしたもの
    yyyyMMdd
    yyyyMMddHH
    yyyyMMddHHmm
    yyyyMMddHHmmss
    yyyyMMddHHmmssSSS
    上記の各フォーマットについて、末尾にタイムゾーン([ISO 8601] +99:99、+9999、+99)を加えたもの
  • desc型は降順ソートを行うための項目となります。{降順ソートしたい項目名}_desc(desc)と指定してください。
    • 例えば、prop1_desc(desc)であれば、prop1の値で降順ソートされます。値は . , / - を除いた数字に変換されます。内部的にはLong.MAX_VALUE - {値}をセットします。
  • {} はMapを意味します。括弧の中は多重度を示し、省略すると1になります。
  • 項目名の最後に!を付けると必須項目となります。
    • 注意:!は型や繰り返しよりも後に記述します。=の直前です。
  • 多重度には子要素の最大繰り返し数を指定します。ただし、int型の場合は最大値、String型の場合は最大の長さになります。
    • また、~を用いることで{最小値~最大値}というように値の範囲を指定できます。
  • =に続けて正規表現を指定することでバリデーションを定義できます。指定した正規表現にマッチしないとエラーとなって入力を受け付けません。
    • 正規表現構文については、Regex Patternを参照してください。
  • $によりXMLの属性やテキストノードを指定することができます。
    • 項目名の先頭が$のものはXMLにシリアライズしたときに属性となります。ただし、属性は同列の他の項目より先に記述する必要があります。
    • $$textを付けることでテキストノードとみなされ、子要素ではなく自身の要素に値を代入します。

T2.スキーマ運用ルール

  • デフォルトでATOM Entry項目が定義されています。つまり、スキーマ定義しなくても、title、subtitle、updatedといったATOMタグが使えます。
    • ATOM項目のみ使うのであればスキーマ定義の必要はありません。逆に定義済みであるATOM項目をテンプレートで定義しようとすると重複エラーとなります。
  • バリデーションルールが指定されている項目はデータ登録時にバリデーションチェックします。
    • チェックできるものは、項目の有無、必須項目、日付や数値などの型、最大最小値と要素数、正規表現などで、合致しなければエラーとして返します。
  • 既にデータが登録されている場合はスキーマの変更ができない場合があります。データをリセットすれば自由に変更可能です。
    • データは要素名が含まれないMessagePackのArray形式で保存しているためテンプレートの変更の際には項目の順番が変わらないように注意する必要があります。
    • 常に要素の最後尾に追記することで順番が保証されます。また途中の階層であっても同列の最後であれば項目を追加できます。

T3.テンプレートサンプル

  • /_settings/templateエントリーの<content>に記述することでスキーマを設定できます。
    id
    email
    verified_email(Boolean) // Boolean型 
    name
    given_name
    family_name
    error
     errors{2}       // インデントでerrorの子要素。Mapで多重度は2
      domain
      reason
      message
      locationType
      location
     code(int){1~100}       // 1~100の範囲     
     message
    subInfo
     favorite
      $attribute       // $で始まる項目はXMLの属性となる(項目の先頭に記述する)
      food!=^.{3}$    // !で必須項目を示す。もし{}や[]があればそれよりも後に記述する。 food[]!=xxx など
      music=^.{5}$    
     favorite2
      food
       food1
     favorite3
      food
     hobby{}
      $$text           // $$textはXMLのテキストノードになる

T4.Index、暗号化、項目ACL

  • /_settings/templateエントリーの<rights>に記述することでIndex項目と暗号化項目の指定ができます。
    • <rights>は暗号化項目であり、サービス管理権限(/_group/$adminグループ)の読込・書込権限が付いています。
  • 項目名に続けて:(コロン)の後に正規表現を記述することでIndexを指定できます。登録するURIのうち正規表現にマッチするものをIndexとして登録します。URIにはEntryの実体(self)や別名(alias)などを指定します。
    • 作成できるIndexの数には上限があります。書込パフォーマンスを向上させるため不必要なIndexは作成すべきではありません。
  • 項目名に続けて=の後にACLを指定できます。ACLは{uid|group}+{RW}の形式でユーザおよびグループの読込(R)書込(W)権限を指定することができます。,(カンマ)で複数件指定できます。
  • 項目名に続けて#を付けると暗号化項目となります。Index項目と暗号化項目はどちらか一つを指定できます。
    • 暗号化は、内部に持っている秘密Key(usersecret)とidと組み合せてハッシュ化したものが実際の暗号Keyとして使われます。
    subInfo.favorite.food:/master|/data // :の後に正規表現を指定することでIndex項目となる(/masterまたは/data)
    subInfo.favorite.music#        // #で暗号化項目(Indexか暗号化かはどちらか一つだけ指定可能)
    subInfo.favorite.food=3+W,/grp1+W,/*+R  // ユーザ3とgrp1の書込権限、および全員(*)に読込権限
    subInfo.favorite3.food:/[0-9]+/(self|alias)=1+W  // IndexとACL権限は同時につけられる
    subInfo.favorite2.food.food1#=1+W  // 暗号化とACL権限は同時につけられる
  • ATOM項目については以下のようなデフォルト設定がされています。
    title:^/$|^/@[^/]*$          // ルートおよびサービス直下のtitleのIndex指定(任意のサービス名)
    contributor=@+RW,/_group/$admin+RW // contributorは自身と/_group/$adminグループのRW権限
    contributor.uri#             // contributor.uriの暗号指定  
    rights#=@+RW,/_group/$admin+RW   // rightsの暗号指定、自身と/_group/$adminグループのRW権限

T5.スキーマに対応したJSONのサンプル

  • 上記テンプレートを元に生成したJSONの例です。
  • この例では、errorを含む全ての項目に値を代入していますが、値が代入されていないものは出現しない項目となります。
  • 実際にはAPIのメソッドごとに第一階層の項目(例えばerrorやsubInfoなど)を選択して使うことになると思われます。
    {
        "feed": {
            "entry": [
                {
                    "email": "email1", 
                    "error": {
                        "code": 100, 
                        "errors": [
                            {
                                "domain": "com.google.auth", 
                                "location": "Authorization", 
                                "locationType": "header", 
                                "message": "invalid header", 
                                "reason": "invalidAuthentication"
                            }
                        ], 
                        "message": "Syntax Error"
                    }, 
                    "family_name": "管理者Y", 
                    "given_name": "X", 
                    "name": "管理者", 
                    "subInfo": {
                        "favorite": {
                            "food": "カレー", 
                            "music": "ポップス1"
                        }
                    }, 
                    "verified_email": false
                }, 
                {
                    "email": "email1", 
                    "error": {
                        "code": 100, 
                        "errors": [
                            {
                                "domain": "com.google.auth", 
                                "location": "Authorization", 
                                "locationType": "header", 
                                "message": "invalid header", 
                                "reason": "invalidAuthentication"
                            }
                        ], 
                        "message": "Syntax Error"
                    }, 
                    "family_name": "管理者Y", 
                    "given_name": "X", 
                    "name": "管理者", 
                    "subInfo": {
                        "favorite": {
                            "food": "カレー", 
                            "music": "ポップス1" 
                            ]
                        }
                    }, 
                    "verified_email": false
                }
            ]
        }
    }

T6.スキーマに対応したXMLのサンプル

  • 上記テンプレートを元に生成したXMLの例です。
    <feed>
      <entry>
        <email>email1</email>
        <verified_email>false</verified_email>
        <name>管理者</name>
        <given_name>X</given_name>
        <family_name>管理者Y</family_name>
        <error>
          <errors>
            <domain>com.google.auth</domain>
            <reason>invalidAuthentication</reason>
            <message>invalid header</message>
            <locationType>header</locationType>
            <location>Authorization</location>
          </errors>
          <code>100</code>
          <message>Syntax Error</message>
        </error>
        <subInfo>
          <favorite>
            <food>カレー</food>
            <music>ポップス1</music>
          </favorite>
        </subInfo>
      </entry>
      <entry>
        <email>email1</email>
        <verified_email>false</verified_email>
        <name>管理者</name>
        <given_name>X</given_name>
        <family_name>管理者Y</family_name>
        <error>
          <errors>
            <domain>com.google.auth</domain>
            <reason>invalidAuthentication</reason>
            <message>invalid header</message>
            <locationType>header</locationType>
            <location>Authorization</location>
          </errors>
          <code>100</code>
          <message>Syntax Error</message>
        </error>
        <subInfo>
          <favorite>
            <food>カレー</food>
            <music>ポップス1</music>
          </favorite>
        </subInfo>
      </entry>
    </feed>

N.通知機能

  • ここでは、メールやWebHook、WebSocket、MobilePushなどの通知機能について説明します。

N1.メール送信

  • メール送信は、Entryの内容をメール送信する機能です。
    • Entryのlinkタグのrel="via"属性、type="email"属性を指定すると、このエントリーがPOST、PUTされた際にメール送信します。
      <link rel="via" type="email" />
    • title属性にPOST、PUT、DELETEを指定することにより、配下のEntryが登録・更新・削除された場合に動作するようになります。 ただし、titleを指定した場合は配下が対象であり自身のEntryについては対象になりません。
      <link rel="via" type="email" title="{Method}" /> 
送信内容
  • Entryの以下の内容を送信します。
    • <title> : 題名
    • <summary> : 本文
      • 本文中に${URL}の文字列がある場合、ノードのURLに置き換えます。
      • 本文中に${RXID=Key}の文字列がある場合、送信先のアカウントのRXIDを含むURLに置き換えます。詳しくは、ワンタイムトークン(RXID) を参照してください。
      • 本文中に${LINK=Key}の文字列がある場合、送信先のアカウントのLinktokenを含むURLに置き換えます。詳しくは、Linktoken を参照してください。
送信先
  • Entryの<subtitle>に送信先をカンマ区切りで指定します。
  • メールアドレス形式の場合、指定されたメールアドレスに送信します。
  • uidの場合、指定されたuidのユーザ名のメールアドレスに送信します。
  • subtitleの指定がない場合、更新されたEntryの参照権限を持つユーザのメールアドレスに送信します。
    • 任意ユーザ(*)やログインユーザ(+)へは送信しません。
    • グループメンバにも送信します。
送信先の優先順位
  • 送信先については以下の優先順位に従ってどれか一つが選択されます。

    1. 更新エントリーのsubtitle

    2. 更新エントリーの参照可能ユーザ

メール受信について
  • メールを受信してFeedとして取得する機能をサーバサイドJavaScriptから使うことができます。
  • 詳しくは、メール受信 を参照してください。

N2.WebHook

  • WebHookはPOSTまたはPUTなどのイベントをトリガーに非同期に外部サービスへのPOST呼び出しを実行します。
    • クライアントからリクエストを受け付けるとTaskQueueに登録して後続の処理を実行します。非同期処理によりクライアントをブロックしません。
    • Entryのlinkタグのrel="via"、type="webhook"を指定し、href属性に送信先URLを指定することで動作します。
      <link rel="via" href="http://foo/" type="webhook" />
  • title属性にPOST、PUT、DELETEを指定すると、配下のEntryが登録・更新・削除されたときに動作するようになります。 ただし、titleを指定した場合は配下が対象であり自身のEntryについては対象になりません。
    <link rel="via" href="http://foo/" title="POST,PUT" />
    • hrefのURLに?async={数字}パラメータが設定されている場合、{数字}秒後にリクエストを実行します。
    • URLパラメータ付いている場合、後続の処理にそのままパラメータを付けてリクエストします。
  • POST呼び出しで渡されるEntryにはselfKeyだけが記述されています。レスポンスは受け取りません。

N3.WebSocket

  • WebSocketは、EntryへのPOSTまたはPUTの操作をトリガーにWebSocketを実行する機能です。(※)β版ではまだ動作しません。
    • Entryのlinkタグのrel="via"とtype="websocket"を指定することで動作します。
      <link rel="via" type="websocket" />
  • title属性にPOST、PUT、DELETEを指定することにより、配下のEntryが登録・更新・削除された場合に動作するようになります。 ただし、titleを指定した場合は配下が対象であり自身のEntryについては対象になりません。
  • 更新したEntryの参照権限(R権限)のあるユーザに対してWebSocketで通知します。
通知内容
  • 更新したEntryのKey(link rel="self")をテキスト形式で通知します。
    • DELETEまたは削除されたaliasの場合、"DELETE " + Keyを通知します。
  • aliasで参照しているユーザには、アクセス可能なlinkを通知します。
    • self、aliasのどちらも参照権限がある場合、selfのlinkが通知されます。
  • WebSocketの通知を受け取ったクライアントでさらに情報が必要な場合は送信されたKeyを使ってGET処理を実行する必要があります。
対象ユーザ
  • そのEntryの参照権限を持つユーザが対象となります。
    • リクエストした本人には通知しません。
    • 任意ユーザ(*)やログインユーザ(+)へは通知しません。
    • グループメンバにも通知します。
    • aliasが外されたユーザおよび付けられたユーザに対し通知します。

N4.MobilePush

設定方法
  • 以下のように、Entryのlinkタグのrel="via"とtype="push"を指定することで動作します。 (※ β版は動作しません)
    <link rel="via" type="push" title="{Method}" /> : 配下のエントリーが指定のメソッドで処理された場合
    <link rel="via" type="push" /> : title属性が指定なしの場合、対象エントリーがPOST、PUTされた場合
  • MobilePush機能を使うには、Accesstokenを使って認証する必要があります。Accesstokenについては、Accesstoken を参照してください。
通知内容
  • <link rel="related" type="push" href="{キー}" /> で指定したキーのエントリーの<summary>内容を通知します。
    • 省略された場合は指定されたエントリー、または更新されたエントリー自身の<summary>内容を通知します。
  • GCM と ADM は通知内容にkey-valueの値を指定できるため、<summary>のテキストに「{key}={value},{key}={value}, ... 」で通知内容を記述します。
    • 通知内容をプレーンテキストで指定した場合、message={通知内容}で通知します。
    • APNSはプレーンテキストのみ指定可です。
  • collapse_key (ADMは consolidationKey、APNSは該当項目なし) はidとします。
  • その他の設定項目は、<subtitle>のテキストに「{key}={value},{key}={value}, ... 」で設定値を指定します。
  • <summary>(メッセージ文)と<subtitle>(設定項目)の採用エントリー
    • <link rel="related" type="push" href="{キー}" /> 指定がある場合、そのエントリーの内容で通知します。
    • 上記設定が無い場合、<link rel="via" type="push"> で指定されたエントリーの内容で通知します。
    • 上記設定が無い場合、更新されたエントリー自身の内容で通知します。
      • <summary>と<subtitle>はそれぞれ別に内容を取得します。
対象ユーザ
  • エントリーのR権限を持つユーザが通知対象です(WebSocketと同じ)
  • プッシュ通知は、プッシュ通知指定したエントリーを参照できるユーザ全てが通知対象です。
  • プッシュ対象ユーザのうち、GCMトークンが登録されているユーザに対し通知されます。
    • ただし、POSTした本人には通知されないようにしています。本人かどうかは、GCMトークンが一致しているかどうかで判断しています。
  • 1ユーザが複数の端末で使用されることは可能ですが、1端末で複数ユーザを並行して使用できません。(都度アクセスキーの再取得が必要です)
    • 1つの(GCMトークンなどの)PLATFORM-TOKENにつき、1件のAccesstokenが登録されます。同じPLATFORM-TOKENは複数件登録されず上書きされます。(Accesstokenについては、Accesstokenを参照してください)
対象デバイス
  • registrationId (Device Token)が通知対象です。

J.サーバサイドJavaScript

  • ここではサーバサイドJavaScriptの仕組みと使い方について説明します。

J1.実行の仕組みと定義方法

  • /s/{スクリプト名}にアクセスすることで、/serverフォルダに格納されている {スクリプト名}.js をサーバで上で読み込み実行します。
    • 実行時間が60秒を超えると強制的にキャンセルされます。
  • サーバサイドJavaScript(例:index.js)には以下のようにECMAScript2015/ES6で書かれたソースコードを記述できます。これは、トランスパイラBabelによって、ECMAScript5の形式に変換されます。
    // index.js
    import reflexcontext from 'reflexcontext' 
    import Person from './person'
    
    const person = new Person('Steve')
    reflexcontext.log(person.say())
    reflexcontext.sendMessage(200, person.say())
    /* @flow */
    // person.js
    export default class Person {
            name:string
            constructor(
                    name:string = 'dummy'
            ) {
            this.name = name
            }
    
            say():string {
            return 'Hello, I\'m ' + this.name + '!!'
            }
    }
サーバサイドレンダリング
  • Reactの機能を使うことでサーバサイドレンダリングが可能になります。
  • /s/ssr.htmlにアクセスすると、「Hello, World」が表示されます。
    //ssr.html.js
    import reflexcontext from 'reflexcontext' 
    import React from 'react'
    import ReactDOMServer from 'react-dom/server'
    
    const element = (
            <h3> Hello, World! </h3> 
    )
    
    const html = ReactDOMServer.renderToStaticMarkup(element)
    
    reflexcontext.doResponseHtml(html)

J2.ReflexContext APIs

  • サーバサイドJavaScriptからReflexContextを介して様々なAPIを実行することができます。
  • 以下は/fooエントリを取得してJSONで返す例です。ReflexContext.doResponse()によりオブジェクトをJSONなどに変換して返します。
  • コンテンツを返すには、doResponseHtml()を使いましたが、JSONなどのデータFeedを返すには、doResponse()を使います。
    • doResponse()は、リクエストパラメータにデータタイプを指定することで様々なデータ形式に変換することができます。例えば、/registration?xでxml、/registration?mでMessagePackを返すことができます。
    import reflexcontext from 'reflexcontext' 
    
    const feed = reflexcontext.getFeed('/registration')  // デフォルトではJSONが返る
    reflexcontext.doResponse(feed) 
ReflexContext メソッド一覧
  • 以下はreflexcontext.jsから呼ばれているnativeのメソッドです。通常のアプリケーションは、reflexcontext.jsのインターフェースを介します。詳細については、ReflexContext JSdoc を参照してください。
  • リクエスト情報取得
    メソッド 説明
    reflexcontext.getRequest() リクエストオブジェクト(feed.entry[0] ~ feed.entry[n])を取得する
    reflexcontext.getPathinfo() PATHINFO(リクエストURLのパス)を取得する
    reflexcontext.getQuerystring() クエリストリングを取得する
    reflexcontext.httpmethod() HTTPメソッド(GET,POST,PUT,DELETE)を取得する
    reflexcontext.getUriAndQuerystring() PATHINFO+クエリストリングを取得する
    reflexcontext.getContentType() Content Typeを取得する
    reflexcontext.getHeaders() リクエストヘッダを取得する
    reflexcontext.getCookies() Cookieを取得する
    reflexcontext.getQueryString(param) URLパラメータを取得する
    reflexcontext.uid() uidを取得する
    reflexcontext.getSettingValue(key) keyを指定してサービス設定情報を取得する
  • データ操作
    メソッド 説明
    reflexcontext.getEntry(uriAndQuery) Entryを取得する。キーとクエリパラメータを指定する
    reflexcontext.getFeed(uriAndQuery) Feedを取得する。キーとクエリパラメータを指定する
    reflexcontext.count(uriAndQuery) 件数を取得する。キーとクエリパラメータを指定する
    reflexcontext.post(feed,uri) 親フォルダuriを指定してfeedをPOSTする
    reflexcontext.post(feed) feedをPOSTする
    reflexcontext.put(feed) feedをPUTする。戻り値は更新後のFeed
    reflexcontext.delete(uri,revision) uriのrevisionのentryを削除する
    reflexcontext.deleteFolder(uri) uriとその配下のentryを削除する
    reflexcontext.saveFiles(props) props(Map)に従ったファイル名で保存する
    reflexcontext.getHtml(uri) HTMLを取得する
    reflexcontext.getContent(uri) コンテンツを取得する
    reflexcontext.getCsv(header[],items[],parent,skip,encoding) アップロードされたCSVをJSONに変換して取り出す
  • 採番
    メソッド 説明
    reflexcontext.setids(uri,value) uriの値をvalueにセットする
    reflexcontext.allocids(uri,num) uriで指定された採番数(num)だけ採番する
    reflexcontext.addids(uri,num) uriの採番の値を加算(+num)する
    reflexcontext.rangeids(uri,value) uriの採番範囲を指定する(value=start-end)
  • レスポンス関連
    メソッド 説明
    reflexcontext.setStatus(rc) ステータスコードrcを設定する
    reflexcontext.setHeader(name,value) レスポンスヘッダを設定する
    reflexcontext.sendRedirect(location) リダイレクトを設定する
    reflexcontext.sendError(rc) ステータスコードを設定する
    reflexcontext.sendError(rc,msg) ステータスコード、メッセージを設定する(HTTP)
    reflexcontext.sendMessage(rc,msg) ステータスコード、メッセージを設定する(JSON)
    reflexcontext.doResponse(feed[,sc]) feed.entry[0] ~ feed.entry[n]を出力する。ステータスコードscを指定可能。リクエストパラメータで?x(xml)、?m(messagepack)を指定可能
    reflexcontext.doResponseHtml(html) htmlを返却する
  • ログ
    メソッド 説明
    reflexcontext.log(title[,subtitle][,message]) ログに記録する
  • PDF、XLS出力
    メソッド 説明
    reflexcontext.toPdf(feed,template,filename) PDFを出力する
    reflexcontext.toPdf(feed,template,filename,base) PDFを出力する(PDF合成)
    reflexcontext.toXls(feed,template,filename) XLSを出力する
  • メール受信
    メソッド 説明
    reflexcontext.getMail(settings) メールを受信する(settings:メール設定)

J3.saveFiles

  • reflexcontext.saveFiles(props)は、ファイルアップロードにおけるMultipart Postリクエストをサーバ側で処理するために使用します。
  • 具体的には、saveFiles(param)で指定するparamオブジェクトに、inputタグのnameをキーにして任意のファイル名を設定します。これは以下のように複数ファイルにも対応しています。
  • アップロードファイルが一つでかつキーを指定しない場合(saveFilesを実行しない場合)、デフォルトではファイル名が写真エントリのKeyになります。
  • 以下の例では、クライアントから2つの画像データが送信されサーバに登録されます。
  • サーバ側(savefiles.js)
    import reflexcontext from 'reflexcontext' 
    
    const param = new Object()
    param.picture1 = reflexcontext.getQueryString('key1')
    param.picture2 = reflexcontext.getQueryString('key2')
    
    reflexcontext.saveFiles(param)
  • クライアント側(upload_pictures.sample.js)
    /* @flow */
    import axios from 'axios'
    import React from 'react'
    import ReactDOM from 'react-dom'
    import {
            Form,
            FormGroup,
            FormControl,
            Button
    } from 'react-bootstrap'
    
    type InputEvent = {
            target: any,
            preventDefault: Function  
    } 
    
    class UploadPictureForm extends React.Component {  
            constructor() {
                    super()
                    this.state = { picture1: {}, picture2: {} }    
            }
    
            handleChange(e:InputEvent) {
    
                    const file: File = e.target.files.item(0)
                    const key = '/registration/'+encodeURIComponent(file.name)
                    const name = e.target.name
    
                // 画像以外は処理を停止
                    if (! file.type.match('image.*')) {
                            return
                    } else {
                            // 画像表示
                            let reader = new FileReader()
                            reader.onload = () => {
                                    this.setState({ [name]: { value: reader.result, key: key } }) 
                            }
                            reader.readAsDataURL(file)                      
                    }
            }       
     
            handleSubmit(e:InputEvent){
                    e.preventDefault()
    
                    const formData = new FormData(e.currentTarget)
                    const param = (this.state.picture1.key ? 'key1='+this.state.picture1.key+'&' : '') +
                                            (this.state.picture2.key ? 'key2='+this.state.picture2.key : '')
    
                    // 画像は、/d/registration/{key} としてサーバに保存されます
                    axios({
                            url: '/s/savefiles?'+param,
                            method: 'post',
                            headers: {
                                    'X-Requested-With': 'XMLHttpRequest'
                            },
                            data : formData
    
                    }).then(() => {
                            alert('success')
                    }).catch((error) => {
                            if (error.response) {
                                    alert('error='+JSON.stringify(error.response))
                            } else {
                                    alert('error')
                            }
                    })
                    
            }
    
            render() {
                    return (
                            <Form horizontal onSubmit={(e) => this.handleSubmit(e)}>
                                    <img src={this.state.picture1.value} />
                                    <br/>
                                    <img src={this.state.picture2.value} />
                                    <br/>
                                    <FormGroup>
                                            <FormControl type="file" name="picture1" onChange={(e) => this.handleChange(e)}/>
                                    </FormGroup>
                                    <FormGroup>
                                            <FormControl type="file" name="picture2" onChange={(e) => this.handleChange(e)}/>
                                    </FormGroup>
                                    <FormGroup>
                                            <Button type="submit" className="btn btn-primary">
                                    登録
                                            </Button>
                                    </FormGroup>
                            </Form>
                    )
            }
    }
    
    ReactDOM.render(<UploadPictureForm />, document.getElementById('container'))

J4.toPDF

  • reflexcontext.toPDF(feed,template,filename)は、テンプレートHTML(template)とデータ(feed)を元にPDFを出力します。
    • テンプレートHTMLにデータをマッピングする仕組みについては、Reflex iTextを参照してください。
  • PDF出力サンプル(hello.pdf.js)では、 /s/hello.pdfにアクセスすると、「Hello, World」が表示されたpdfがダウンロードされます。
    import reflexcontext from 'reflexcontext' 
    
    // リソースデータ取得
    const data = {'feed': {'entry': [{'title': 'Hello World'}]}}
    
    // PDF出力
    reflexcontext.toPdf(data, '/pdf/hello_world.html', 'test.pdf')
  • テンプレートHTML(hello_world.html)
    <?xml version="1.0" encoding="UTF-8" ?>
    <html>
    <body>
    
        <!-- 用紙サイズ指定:(A4・縦) -->
    
        <div class="_page" style="pagesize:A4; orientation:portrait;">
            <table>
                <tr>
                    <td>
                        <!-- 表示させたい値の項目名をidに定義します -->
                        <p id="title" />
                    </td>
                </tr>
            </table>
        </div>
    </body>
    </html>
  • また、テンプレートHTMLはReactのSSR機能を使うことで動的にPDFを生成することができます。
  • PDF出力サンプル(ssr.pdf.js)では、 /s/ssr.pdfにアクセスすると、「Hello, Harper Perez」が表示されたpdfがダウンロードされます。
    import reflexcontext from 'reflexcontext' 
    import React from 'react'
    import ReactDOMServer from 'react-dom/server'
    
    function formatName(user) {
            return user.firstName + ' ' + user.lastName
    }
    
    const user = {
            firstName: 'Harper',
            lastName: 'Perez'
    }
    
    const element = (
            <html>
                    <body>
                            <div className="_page" style={{ pagesize: 'A4', orientation: 'portrait'}}>
                                    <table>
                                            <tr>
                                                    <td>
                                                            <p> Hello, {formatName(user)}! </p> 
                                                    </td>
                                            </tr>
                                    </table>
                            </div>
                    </body>
            </html>
    )
    
    const html = ReactDOMServer.renderToStaticMarkup(element)
     
    // PDF出力
    reflexcontext.toPdf({}, html, 'test.pdf')

J5.toXls

  • reflexcontext.toXls(feed,template,filename)は、テンプレートXLS(template)とデータ(feed)を元にEXCELデータ(XLS)を出力します。
  • xls出力サンプル(hello.xls.js)では、 /s/hello.xlsにアクセスすると、動的に生成されたxlsがダウンロードされます。
    // hello.xls.js
    import reflexcontext from 'reflexcontext' 
    
    const data = {'feed' : {'entry' : [{'userinfo' : {'id' : 123,'email' : 'foo@bar.com'}},{'favorite' : {'food' : 'ラーメン','music' : ['ジャズ','ポップス','ロック']}}]}}
    
    // XLS出力
    reflexcontext.toXls(data, '/xls/person_template.xls', 'test.xls')

J6.メール受信

  • reflexcontext.getMail(settings)は、指定した設定情報(settings)を元にメールを受信します。
  • 受信結果はfeedのentryに格納されます。複数件受信すると複数件のentryが返ります。
  • feed.entryの各項目には以下のようにメールの情報がセットされます。()が対応するメールの情報
    • title(subject)
    • subtitle(cc)
    • summary(メール本文)
    • content(添付ファイル) ※ Base64に変換されます
      • content.type(ファイルの形式)
      • content.src(ファイル名)
  • mail受信サンプル(getmail.js)では、/s/getmailにアクセスすると、Yahoo!メールの設定情報を元にメールを受信します。
    import reflexcontext from 'reflexcontext' 
    
    const settings = new Object()
     
    // 基本設定(例:yahooメール)
    settings['mail.pop3.host']='pop.mail.yahoo.co.jp'
    settings['mail.pop3.port'] = '995'
    // タイムアウト設定
    settings['mail.pop3.connectiontimeout'] = '60000'
    //SSL関連設定
    settings['mail.pop3.socketFactory.class']='javax.net.ssl.SSLSocketFactory'
    settings['mail.pop3.socketFactory.fallback']='false'
    settings['mail.pop3.socketFactory.port'] = '995'
    
    settings['username']='xxxxx@yahoo.co.jp'
    settings['password']='xxxxx'
    
    const result = reflexcontext.getMail(settings)
    reflexcontext.log(JSON.stringify(result))

J7.CSV受信

  • ReflexContext.getCsv(header[],items[],parent,skip,encoding)は、指定した情報を元にCSVを受信します。
  • header[]にはCSVのヘッダ情報を指定します。受信したCSVファイルのヘッダ情報と異なれば以下のようにパースエラーとなります。
    {"feed":{"entry":[{"title" : "Header parse error"}]}}    
  • itemsには対応するJSONの項目名を指定します。
    • item1(int)やitem1(boolean)のようにカッコの中にintやbooleanの型を指定できます。
  • parentは親項目を指定します。
  • skipはCSVファイルの読み飛ばす行数を指定します。
  • encodingはCSVファイルの文字コードを指定します。(UTF-8,SJIS等)
  • サーバ側(getcsv.js)
    import reflexcontext from 'reflexcontext' 
    
    const items = ['item1','item2(int)','item3(int)']
    const header = ['年月日','件数','合計']
    const parent = 'order'
    const skip = 1
    //const encoding = 'SJIS'
    const encoding = 'UTF-8'
    
    // CSV取得
    const result = reflexcontext.getCsv(header,items,parent,skip,encoding)
    reflexcontext.log(JSON.stringify(result)) // {"feed":{"entry":[{"order":{"item1":"2021/7/5","item2":3,"item3":3}},{"order":{"item1":"2021/7/6","item2":5,"item3":8}},{"order":{"item1":"2021/7/7","item2":2,"item3":10}}]}}
  • csvデータ(csv/sample.csv)
    // この1行はskipする
    年月日,件数,合計
    "2021/7/5",3,3
    "2021/7/6",5,8
    "2021/7/7",2,10
  • クライアント側(upload_csv_sample.js)
    /* @flow */
    import axios from 'axios'
    import React from 'react'
    import ReactDOM from 'react-dom'
    import {
            Form,
            FormGroup,
            FormControl,
            Button
    } from 'react-bootstrap'
    
    type InputEvent = {
            target: any,
            preventDefault: Function  
    } 
    
    class UploadCsvForm extends React.Component {  
            constructor() {
                    super()
                    this.state = { }    
            }
     
            handleSubmit(e:InputEvent){
                    e.preventDefault()
    
                    const formData = new FormData(e.currentTarget)
    
                    // 画像は、/d/registration/{key} としてサーバに保存されます
                    axios({
                            url: '/s/getcsv',
                            method: 'post',
                            headers: {
                                    'X-Requested-With': 'XMLHttpRequest'
                            },
                            data : formData
    
                    }).then(() => {
                            alert('success')
                    }).catch((error) => {
                            if (error.response) {
                                    alert('error='+JSON.stringify(error.response))
                            } else {
                                    alert('error')
                            }
                    })
                    
            }
    
            render() {
                    return (
                            <Form horizontal onSubmit={(e) => this.handleSubmit(e)}>
                                    <FormGroup>
                                            <FormControl type="file" name="csv" />
                                    </FormGroup>
                                    <FormGroup>
                                            <Button type="submit" className="btn btn-primary">
                                    登録
                                            </Button>
                                    </FormGroup>
                            </Form>
                    )
            }
    }
    
    ReactDOM.render(<UploadCsvForm />, document.getElementById('container'))

H.HTTPステータスコードとメッセージ

  • ここではHTTPステータスコードと意味について説明します。

200(OK) 成功

  • "OK."
    • 処理が正しく実行された

201(CREATED) 生成

  • "Created."
    • リソースが新しく生成された

204(NO_CONTENT) コンテンツなし

  • "No entry."
    • リソースがなかった

206(Partial Content) 部分的内容

  • "Partial Content."
    • 返された結果が一部である

400(BAD_REQUEST) リクエスト不正

  • "Request object is invalid."
    • 不正なリクエストが送信された
    番号 メッセージ 意味
    1 XX is required. XXの指定が必要
    2 XX is not available. XXが使えない
    3 XX does not exist. XXが存在しない
    4 XX is invalid. XXが不正
    5 Allocate id must be a numeric value. 正しい数値がIDに指定されていない
    6 Callback strings must use alphanumeric characters. callbackには英数字以外使用不可
    7 Duplicated Link self. Link selfが重複している
    8 Duplicated rules for ACLs. 既に同じ権限が設定されている
    9 Duplicated URIs for URIが重複している
    10 Forbidden request to this service. このサービスへの許可されないリクエスト
    11 Max must be greater than min. 最大値が最小値より大きい値ではない
    12 Must specify a 'E'(External) control. E権限を指定する必要がある
    13 Not allowed to cancel the process. プロセスをキャンセルできない
    14 Not allowed to use an alias for bulkcopy. bulkcopyではaliasは使えない
    15 Optimistic locking failed for the specified template. 指定したテンプレートの更新エラー(楽観的排他エラー)が発生
    16 Password must be contain at least 8 characters, including at least 1 number and includes both lower and uppercase letters. passwordは1文字以上で数字と小文字と大文字混じりである必要がある
    17 Request format is invalid: XX リクエストのフォーマットが正しくセットされていない
    18 Revision number must be a numeric value. リビジョンが数字ではない
    19 Specified value is out of range. リビジョンの値が範囲外
    20 Specified URI does not match the id nor key. 指定したURIがIDとKeyに一致しない
    21 The first limit must be less than limit(XX). first limitはlimit(XX)以下を指定しなければならない
    22 The first limit must be more than 0. first limitは0以上を指定しなければならない
    23 The number of pages must be more than 0. number of pagesは0以上を指定しなければならない
    24 Too many entities. entityの数が多すぎる
    25 Unauthorized request to modify the auth. Authの変更リクエストは受け付けられない
    26 URI must not contain any prohibited characters. URIに許可していない文字の使用は不可
    27 URI must not contain any white-space characters. URIにblank文字は使えない
    28 URI must start with a slash. URIは/から始まるものでなければならない
    29 Accesskey and Accesstoken can not be used. AccesskeyとAccesstokenが使えない
    30 Please set only one key. selfは1エントリ1件のみ
    31 Please make a pagination index in advance. 先にpagination indexを作成する必要がある
    32 Session is disabled. セッションが無効になっている
    33 Session does not exist. セッションが存在しない
    34 Top entry can not be specified. ルートエントリは指定不可
    35 Forbidden request to this service. このサービスで実行できない
    36 Service init entry is nothing. サービス初期化エントリが存在しない
    37 .js' is not found. .jsファイルが見つからない
    38 Service does not exist. サービスが存在しない
    39 The Web Application has not been activated. Webアプリケーションが有効になっていない

401(UNAUTHORIZED) 認証エラー

  • "Authentication error."
    • 認証に失敗
    番号 メッセージ 意味
    1 Authentication is locked. 認証がロックされている
    2 Authentication time out. 認証タイムアウト
    3 Remote access is not allowed. リモートからのアクセスは禁止
    4 Captcha required at next login. 次回からCaptcha認証が必要

403(FORBIDDEN) 認可エラー

  • "Access denied."
    • 認証は成功しているが認可でエラー

404(NOT_FOUND) コンテンツが見つからない

  • "No entry."
    • コンテンツが見つからなかった

406(NOT_ACCEPTABLE) 認証タイムアウト

  • "Authentication time out."
    • 認証タイムアウトが発生した

409(CONFLICT) 競合発生

  • "Conflict"
    • キー重複、もしくは楽観的ロック失敗
    番号 メッセージ 意味
    1 Duplicated primary key. Keyが重複している
    2 Alias is duplicated. aliasが重複している
    3 User is already registered. ユーザが既に登録されている
    4 Optimistic locking failed. 更新エラー(楽観的排他エラー)

412(PRECONDITION_FAILED) 署名検証エラー

  • "The signature is invalid."
    • 署名の検証において失敗した(署名が不正)

413(REQUEST ENTITY TOO LARGE) リクエストサイズエラー

  • Request Entity Too Large."
    • リクエストデータのサイズ超過

417(EXPECTATION_FAILED) リクエストセキュリティエラー

  • "Request security error."
    • XMLHttpRequestからのリクエストではないがJSONで出力しようとしている
      • 「X-Requested-With : XMLHttpRequest」ヘッダが設定されていないとエラー

423(LOCKED) 二重ログインエラー、バルクコピー中エラー

  • "Authentication is locked."
    • 二重ログインが発生した。またはバルクコピー中でロックが発生した
    番号 メッセージ 意味
    1 The service is currently not available due to registration process.Please try again later. バルクコピー中につき使用できない。後ほど実行してください。

424(FAILED_DEPENDENCY) サービス停止または未登録エラー、サーバサイドJavaScriptエラー

  • "Not in service."
    • サービスを利用できない
  • サーバサイドJavaScriptエラー
    • サーバサイドJavaScriptでエラーが発生した

500(INTERNAL_SERVER_ERROR) 内部サーバーエラー

  • Exceptionの内容
    • サーバにおける致命的エラー