2011年12月4日日曜日

Groovy+HttpClient で Redmine の wiki 更新

Redmine の wiki を、HTTP のクライアントプログラムで更新する方法を考えてみた。言語は何でもいいが、ここでは Groovy を使ってみた。

====

例えば、以下のようなコードで、

class Test {
   def static content = """\
h1. 大見出し

なんとかこんとか
${ new Date().toString() }
"""
      public static void main(args) {
      new RedmineWikiRewriter()
         .login("user001", "password")
         .startEdit("project001", "Http_post_test")
         .saveEdit(content)
   }
}
下の画像のように、wiki ページが更新されるような クラス RedmineWikiRewriter を考えてみる。

  • login() では、与えられたユーザ名とパスワードでログインして、Redmine から返されるクッキーを保持する。
  • startEdit()は、指定のプロジェクトの指定の wiki ページを開く。これはページのバージョンを得るために必要
  • saveEdit()は、wiki コンテンツをマルチパートの HTTP POST で送信する
コードは以下のようなものになる

class RedmineWikiRewriter {
   def static UTF8 = Charset.forName("UTF-8")

   def HTTPBuilder http = new HTTPBuilder('http://localhost:3000/')

   def redmineSession
   def authenticityToken
   def contentVersion 
   
   def project
   def wikiEntry
   
   public RedmineWikiRewriter login(String username, String password) {
      http.get(path : '/login', query : [q:'Groovy'] ) { resp, reader -> 
         authenticityToken = reader.BODY.DIV.DIV
            .findAll { it.id="login-form" }.depthFirst()
            .grep { "authenticity_token"== it.@name.toString() }[0]
            .@value.toString()
      }
      http.request(POST) {
         uri.path = '/login'
         send URLENC, [
            username:username, password:password, 
            authenticity_token:authenticityToken]

         response.success = { resp ->
            redmineSession = resp.getAllHeaders()
               .find {"Set-Cookie"==it.name.toString()}
               .value.split(";")
               .find {it.toString().startsWith("_redmine_session")}
            }
      } 
      this
   }
   RedmineWikiRewriter startEdit(String project, String wikiEntry) {
      this.project = project
      this.wikiEntry = wikiEntry 

      http.request(GET) { resp ->
         uri.path = "/projects/"+project+"/wiki/" + wikiEntry + "/edit"
         headers.'Cookie' = redmineSession
         response.success = { res, reader ->
            contentVersion = reader
               .depthFirst()
               .grep{"content_version"==it.@id.toString() }[0]
               .@value.toString()
         }
      }
      this
   }
   void saveEdit(String content) {
      DefaultHttpClient httpclient = new DefaultHttpClient();
      def uri = "http://localhost:3000/projects/"+ project +"/wiki/" + wikiEntry

      HttpPost httppost = new HttpPost(uri)
      httppost.addHeader('Cookie', redmineSession)
      
      MultipartEntity mpe = new MultipartEntity(
         HttpMultipartMode.BROWSER_COMPATIBLE, 
         "----WebKitFormBoundaryfmexSJQ5daIXdA04", 
         UTF8);
      addPart(mpe, "commit",          "Save");
      addPart(mpe, "project_id",       project);
      addPart(mpe, "action",          "update");
      addPart(mpe, "_method",          "put");
      addPart(mpe, "authenticity_token", authenticityToken)
      addPart(mpe, "id",             wikiEntry)
      addPart(mpe, "content[text]",    content)
      addPart(mpe, "content[version]", contentVersion)
      addPart(mpe, "controller",       "wiki")
      
      httppost.setEntity(mpe);

      httpclient.execute(httppost);
   }
   static void addPart(MultipartEntity entity, String name, String value) {
      entity.addPart(name, new StringBody(value, UTF8));
   } 
}

■ 振り返り

  • saveEdit() でも HttpBuilder を使いたかったが、なぜか思うように動かない。仕方なく、DefaultHttpClient を使用
  • wiki エントリの新規作成でも動くかどうかは未確認
  • たかだかこれくらいのコードでも、やってみると予想外に難しい。Redmine の Rubyコードを読んだり、HTTP の Header を調べたりいろいろ手間がかかった。あと multipart の POST も意外と難しい
  • 仕組みはだいたい分かったので、他の言語でも試してみたい。もともとやりたかったのは、Excel 上の VBA からデータを整形して wiki に載せるというのを自動化すると言う事だった。

0 件のコメント:

コメントを投稿