partuza のコードを追いかけて。

24inchのiMacでも画面が狭いと思うようになった本日。Macユーザーの方って、ソースを読むときどうしているんでしょう?とりあえず、KEdit のウィンドウ大量に開いて読んでいるのですが、読む専用のいいツールってないんですかね*1

それはともかく、Shindig を使ったOpenSocial Containerとして partuza があるので、実装どないなっとんのーということで、追いかける。まずは、/people/{guid}/{selector}/{pid} の実装の感覚(HTTP リクエストからDBのデータを取り出して、HTTP レスポンスにするまで)がつかめれば、あとはどうにでもなる。

というわけで、一気に読みます。


partuza では Shindig と連携するためのクラス群が /Shindig ディレクトリに含まれています。shindig側の shindig/php/config/container.php (またはlocal.php) で以下のように設定をしてあげると、/people/ などのShindigで実装されている OpenSocial REST API のエンドポイントの実装が、Partuza側のデータサービスを呼び出すような構成になる模様。

$shindigConfig = array(
'person_service' => 'PartuzaService',
'activity_service' => 'PartuzaService',
'app_data_service' => 'PartuzaService',
'extension_class_paths' => '/Library/WebServer/Documents/partuza/Shindig'
);

まぁようするにPartuzaServiceの部分をCouchDBからデータを取るように実装してあげれば、それで終了なんでは?というつっこみはスルーして(PHPを書くなんてそんなことやりたくない、宗教上の理由です、仕事じゃないですから)。

となると、person_service の部分をShindig側がどう使っているのか見ておく必要がありますね。ということで、そろそろ本題に。

まずは、shindigの/index.php。これはURIからServletを決定するディスパッチャ。

    Config::get('web_prefix') . '/social/rest' => 'DataServiceServlet',

ですので、ひとまずDataServiceServletへ。/src/social/servlet/DataServiceServlet.php にあります。

  public function doGet() {
    $this->doPost();
  }

  public function doPut() {
    $this->doPost();
  }

  public function doDelete() {
    $this->doPost();
  }

もう、RESTもなにもあったもんじゃないよね、って感じですけどスルーしましょう。RESTの人は、しっかりdisるといいのかもしれません。REST APIっていっておきながら参照実装がこれかよ!的な。。

とりあえず、doPost()追いかけるけれど、気持ちはGETで。まずは GET /people/{guid}/{selector}/{pid} の実装を考えたいから!!

  public function doPost() {
    $xrdsLocation = Config::get('xrds_location');
    if ($xrdsLocation) {
      header("X-XRDS-Location: $xrdsLocation", false);
    }
    try {
      $token = $this->getSecurityToken();
      if ($token == null) {
        $this->sendSecurityError();
        return;
      }
      $inputConverter = $this->getInputConverterForRequest();
      $outputConverter = $this->getOutputConverterForRequest();
      $this->handleSingleRequest($token, $inputConverter, $outputConverter);

oAuth のセキュリティトークンの処理が入って、そのトークンを使ってリクエストのデータ処理を行います。

  /**
   * Handler for non-batch requests (REST only has non-batch requests)
   */
  private function handleSingleRequest(SecurityToken $token, $inputConverter, $outputConverter) {
    //uri example: /social/rest/people/@self   /gadgets/api/rest/cache/invalidate

具合であるので、ここを見れば良さそうです。数行、HTTP_RAW_POST_DATAの取り扱いに関するごにょごにょが続いて、リクエストパラメーターから、レスポンスのデータを決めるところが記述されています。

    $servletRequest['params'] = array_merge($_GET, $_POST);
    $requestItem = RestRequestItem::createWithRequest($servletRequest, $token, $inputConverter, $outputConverter);
    $responseItem = $this->getResponseItem($this->handleRequestItem($requestItem));

array_merge しちゃっている当たりも、なんというか。URI Query と www-form-url-encoded の根本的な違いは何だろう、とか思ったものの。

RestRequestItem は @me とか @self とかのスペシャルなパラメーター(URI パス)を何とかしているのかと期待したいのでスルーして、$this->handleRequestItem のところ。ここは、JSON-RPC-API と REST-API とで互換になるので、親クラスである ApiServlet クラスのほうに記述されています。

  /**
   * Delivers a request item to the appropriate DataRequestHandler.
   */
  protected function handleRequestItem(RequestItem $requestItem) {
    if (! isset($this->handlers[$requestItem->getService()])) {
      throw new SocialSpiException("The service " . $requestItem->getService() . " is not implemented", ResponseError::$NOT_IMPLEMENTED);
    }
    $handler = $this->handlers[$requestItem->getService()];
    return $handler->handleItem($requestItem);
  }

つまり、URIから得られた$requestItem は $requestItem->getService() で呼び出すサービス名が取得できるようになっていて、$this->handlers の連想配列にハンドラーが定義されていて、ハンドラーのhandleItemを呼び出して処理をする。確かに、コンストラクターには、

    $this->handlers[self::$PEOPLE_ROUTE] = new PersonHandler();

な具合に入っていますね。ということで続いて、/src/social/service/PersonHandler.php の handleItem を見ればよいのかと。

その前に、コンストラクタ重要。

class PersonHandler extends DataRequestHandler {

  public function __construct() {
    $service = Config::get('person_service');
    $this->personService = new $service();
  }

ということで、$this->personService は new PartuzaService() ということになりま、、ってPHPって文字列() とかいうことできるの? そういうように読めるんだが、キモイ。

で、handleItem のほうは、実は親クラスのDataRequestHandler.php側に定義されていて、

  public function handleItem(RequestItem $requestItem) {
    try {
      $token = $requestItem->getToken();
      $method = strtolower($requestItem->getMethod());
      if ($token->isAnonymous() && ! in_array($method, self::$GET_SYNONYMS)) {
        // Anonymous requests are only allowed to GET data (not create/edit/delete)
        throw new SocialSpiException("[$method] not allowed for anonymous users", ResponseError::$BAD_REQUEST);
      } elseif (in_array($method, self::$GET_SYNONYMS)) {
        $parameters = $requestItem->getParameters();
        if (in_array("@supportedFields", $parameters)) {
          $response = $this->getSupportedFields($parameters);
        } else {
          $response = $this->handleGet($requestItem);
        }

こっちでは、GET|PUT|POST|DELETEを区別しているんですよね。handleGetで、PersonHandler側に戻ります。

 public function handleGet(RequestItem $request) {
    $request->applyUrlTemplate(self::$PEOPLE_PATH);
    
    $groupId = $request->getGroup();
    $optionalPersonId = $request->getListParameter("personId");
    $fields = $request->getFields(self::$DEFAULT_FIELDS);
    $userIds = $request->getUsers();

まずは /people/{guid}/{selector}/{pid} のパラメーター解析をごにょごにょして、

        // Every other case is a collection response of optional person ids
        return $this->personService->getPeople($personIds, new GroupId('self', null), $options, $fields, $request->getToken());

な具合で、実際のデータサービスを呼び出す運びになっておりました。personServiceの実態はPartuzaServiceなので、やっとこ、partuza側のソースに飛んで、/Shindig/PartuzaService.php の方を見ます。

  public function getPeople($userId, $groupId, CollectionOptions $options, $fields, SecurityToken $token) {
    $ids = $this->getIdSet($userId, $groupId, $token);
    $allPeople = PartuzaDbFetcher::get()->getPeople($ids, $fields, $options, $token);

はい、/Shindig/PartuzaDbFetcher.php に移動。ざっとみると、このスクリプトだけでDB処理はすべて完結している模様。MySQLが相手ですね。

  public function getPeople($ids, $fields, $options, $token) {
    // 略
    $query = "select * from persons where id in (" . implode(',', $ids) . ") $filterQuery order by id ";
    $res = mysqli_query($this->db, $query);
    if ($res) {
      while ($row = @mysqli_fetch_array($res, MYSQLI_ASSOC)) {
        $name = new Name($row['first_name'] . ' ' . $row['last_name']);
        $name->setGivenName($row['first_name']);
        $name->setFamilyName($row['last_name']);
    // 続く

と、DBから取ったレコードからちまちまとpersonオブジェクトを作って、といったことをしています。
で、DBのテーブルはpersons, addresses, emails, ... などいろいろあるようなので(なぜかpeopleではなくpersonsな当たりが謎)、そのテーブルの回数分だけSELECTしているのです。

こりゃ、大変だ。CouchDBでやるなら

{
type: "Person",
emails : ["a@b.com", "b@b.com", ..]
}

と定義しておけるのですごーく気楽にフィルタがかけそうな雰囲気ですよ!さようならSQL

と盛り上がったところで、大体雰囲気はつかめたので終了。

次は @me とか @self とかのスペシャルパラメーター(これなんていうんだろう)が、oAuthのセキュリティトークンとか、データリプレゼンテーションとかとどういう関係があるのかを調べる必要がありそうです。

ここまでOpenSocialのスペックを調べていった感覚だと、これはなかなかよく考えられていて、難しすぎない、いい仕様だなぁ、と思ったのですが実際使われている方から見るとどうなんだろう*2

*1:会社とかだと印刷して赤ペン先生状態で読むのですが、家にはプリンターがないのでw キンコーズにいけばいいのか...?

*2:DMTFのほげほげとか、ベンダー同士の政治の空気を読まないと理解が不可能な難しすぎる仕様群を読むのにもう疲れたので、Webっていいなーって本当に思います