• redmineの プラグインを 2.0用に移行する

    Redmineによるタスクマネジメント実践技法

    redmine 2.0がリリースされて一ヶ月ちょい。
    新featureが出てくる前に自作のプラグインも移行しておかないとなーと思いつつ放置してたんだけど、やっと手をつけた。

    以下のURLを参考にした。

    Redmine 2.0.xにプラグインを移植する
    Redmine2.0へのプラグインのマイグレーションについて


    自分のところでひっかかったこと。

    • RAILS_ROOTが使えない
       → Rails.root
    • RAILS_DEFAULT_LOGGERが使えない
       →Rails.logger
    • RAILS_ROOT/vendor/plugin/が変更された
       →RAILS_ROOT/plugin/
    • routesの定義の仕方
       →ActionController::Routing::Routes.draw からRedmineApp::Application.routes.drawに
      RedmineApp::Application.routes.draw do
        match 'projects/:project_id/wiki/:id/graphviz', :to => 'wiki_graphviz#graphviz'
      end
      
    • テンプレートのファイル名が変わった
       →*.rhtml から *.html.erbに
    • テスト用webサーバ他のコマンドが変わった
       →./script/sever から ./script/rails server に
       →./script/runner から ./script/rails runner に
    • ActionController::Base.cache_configured? がprivateになった
       →意図が違うが ActionController::Base.perform_caching で代替


    ここからはredmine 2.0への移行じゃなくて、ruby 1.8から1.9に変えたことでひっかかったこと。

    • String#eachがなくなった
       →String#each_line とか
    • proc+ブロックで得たProcオブジェクトのブロック内でreturnしたら呼び出し元メソッドがreturnした
      これを知らなくてすごく悩んだ。
      Redmineのviewフック内でコントローラにrender_to_stringをsendしている。
      テンプレート内でクロージャ(proc由来のProcオブジェクト)を使ってtrue/falseを返しているつもりだったのだけど、viewフックメソッドからtrueが返る状態になってしまった。
      lambdaに変更した。


    それぞれremdine-2.0ブランチ
    https://github.com/tckz/redmine-wiki_graphviz_plugin
    https://github.com/tckz/redmine_wiki_preview_ext
    https://github.com/tckz/redmine_wiki_astah


    でも、redmine 2.0系のfeatureが増えてくるまでは、運用環境は1.4系のままかな。

  • Pipesを使って RubyForgeのプロジェクトのリリースを RSSでウォッチする

    libxml-rubyのAPIの変更に気がつかずにはまったので、リリースをウォッチしようとRubyForgeのページを確認するも、フィードが見つからない(あったりして)。
    RubyForge全体のnewsのフィードと、プロジェクトのnewsのフィードはあった。
    だけどリリースとnewsは連動しているわけではないらしく投稿するかしないかは管理者次第のようだ。
    RubyForge: LibXML: ニュース
    RubyForge: LibXML: ファイルリスト
    じゃあPipesでも使ってみるかと気軽に始めたのだけどかなり手間取った。

    やりたいこと
    • libxml-rubyのリリース(「libxml-ruby-0.9.8」「libxml-ruby-0.9.7」部分)を抜き出してフィードに。
    • descriptionに上記文字列のリンク先(リリースノート)の内容を入れる
    最初のリリース一覧の抜出からしてなかなか意図した結果にならない。XPathでさくっと取り出してぇ、と試行錯誤。結局YQLで大枠を抜き出してOperators/Renameでちまちまとtitle要素/link要素にとり出すことに。

    select  * from html
    where
    url="http://rubyforge.org/frs/?group_id=494"
    and
    xpath="/html/body/div[3]/table/tr/td[2]/table/tr[position()>=4 and td/p/strong/text()]"
    得られたtrリストの直下の一番目のtdと二番目のtdを指定する方法でまた悩む。
    item.td.0.p.strong.a.href  →link
    item.td.0.p.strong.a.content →title
    item.td.1.p.strong →日時
    てな感じ。

    リリースノートの内容は、<pre>の中身でざくっと指定できたのでLoop+Fetch Pageの結果をdescriptionにassign。

    日時をフィードのpubDateにどう反映するのかがまたよくわからなくて、他の人のPipesを参考にLoop+Date Builderでitem.y:publishedにassignした。フィード出力の時点でpubDateに突っ込まれるようだ。

    ようやく出来たのが↓のPipes。Livedoor Readerに食わせてウォッチ中。
    http://pipes.yahoo.com/pipes/pipe.info?_id=78e1182d42024a179dfda9fc6f708550

    面白いしすごく便利だけど、やりたいことをPipes流の発想に変換するのがなかなか大変。。。
    YQLを使えばXPathでスクレイピング可能なのが救い。



  • libxml-ruby のI/Fが変わってた

    ふと気づいたらlibxml-rubyが0.9.8になってた。
    gemで0.8.3からupdateしたら動かないスクリプトが出てきた。
    XML::Document#to_s がエラーになる。

    CHANGESを見ると
    • 出力オプションをHash指定に変えた
    • エンコード名を文字列指定からLibXML::XML::Encoding下の定数指定に変えた
    ということらしい。
    前者はともかく後者は面倒だなー。というのもjrubyでも使うスクリプトなのでtransformer.setOutputProperty(javax.xml.transform.OutputKeys::ENCODING, エンコード名な文字列) とI/Fを合わせて文字列指定で作ってたから。

    libxml-rubyのマニュアルによればエンコード名文字列から定数を得るI/Fがあるみたいなんだけど、どうも期待した結果にならない。
    (マニュアル上はInput.s_to_encoding("UTF_8") なんだけどext/libxml/ruby_xml_encoding.cを見るとfrom_sのようで混乱する

    $ irb
    irb(main):001:0> require 'rubygems'
    => true
    irb(main):002:0> require 'libxml'
    => true
    irb(main):003:0> LibXML::XML::Encoding.from_s("UTF_8")
    => -1
    irb(main):004:0> LibXML::XML::Encoding.from_s("UTF-8")
    => false
    irb(main):005:0> LibXML::XML::Encoding.from_s("utf8")
    => false
    irb(main):006:0> LibXML::XML::Encoding.from_s("utf-8")
    => false
    irb(main):007:0> LibXML::XML::Encoding.from_s("utf_8")
    => -1
    irb(main):008:0>

    とりあえず文字列をいじくって定数名とみなしてお茶にごす。
    alias名が困るなー

          const_name = encoding_name.gsub(/-/, "_").upcase
    encoding = LibXML::XML::Encoding.const_get(const_name)
    doc.to_s(:encoding => encoding)

    環境
        - CentOS 5.2 i686
        - libxml2: 2.6.26-2.1.2.7
        - ruby: 1.8.5-5.el5_2.6
        - rubygems: 1.3.1
        - libxml-ruby: 0.9.8 (gem)

  • libxml-ruby 0.8.3が出てた

    ふと、libxml-ruby本家を確認したら、いつのまにか0.8.3になってた。
    RubyForge: LibXML: ファイルリスト
    7月に入ってから怒涛のupdate

    jruby+Java DOMに逃げてなんとかやりすごしていたのだけど、これは期待できるか?と再チャレンジ。
    とりあえず自分ところのアプリの範囲では「[BUG] object allocation during garbage collection phase」で落ちることがなくなった。

    I/Fをlibxml-rubyのままにしておいて正解だった。単にCRubyで実行しただけ。

    やはりlibxml2の性能は魅力。

    OS: CentOS 5.2
    ruby: 1.8.5
    libxml2: 2.6.26
    libxml-ruby: 0.8.3

  • libxml-rubyで [BUG] object allocation during garbage collection phase

    gemでlibxml-ruby(0.5.4)をインストールして使っていた。
    いつからか、以下のようなメッセージでクラッシュしたりしなかったり、という事象が出始めた。
    [BUG] object allocation during garbage collection phase
    ・・・という問題に悩んでいたのだけど、そのものズバリの答えが。
    [ruby-dev:35157] Re: error: Ruby 1.8.7 object allocation during garbage collection phase
    クラッシュといっては心外な話しか。
    新しいrubyだと問題を検出して[BUG]を出す、ということだそう。

    原因はわかったが、とりあえず手元のlibxml-ruby前提で書いたツールをどうするか。

    libxml-rubyのパッチを書く?
    とコードを眺めたものの早々にあきらめた。これはすぐにどうこう出来そうもない。。

    REXMLで書き直す?
    そもそもREXMLが遅いので、libxml-rubyにしただけにこれは避けたい。

    perlのLibXMLで書き直す?
    んー、時間もかかるし同じものをもう一回書くのもな。

    で、jruby
    REXMLを使うとスタート地点に戻ってしまうので、javaのDOM・XPath APIを使うことに。

    とりあえず、libxml-rubyで使ってるインタフェースだけラッパーを用意。
    ツール側のコードはほぼ変更がなかった。
    libxml2の軽快さには遠いがjrubyありがとうって感じ。

    jruby:1.1.2
    Sun Java: 1.6.0_05
    OS:CentOS 5.1

      $KCODE='u'
    require 'java'

    class WrapNode
    attr_reader :node

    def initialize(e)
    @node = e
    @xpath = nil
    end

    def to_s
    @node.nodeValue.to_s
    end

    def wrap(e)
    if e == nil
    return nil
    end
    WrapNode.new(e)
    end

    def next
    self.wrap(@node.nextSibling)
    end

    def text?
    @node.nodeType == org.w3c.dom.Node::TEXT_NODE
    end

    def [](name)
    @node.getAttribute(name)
    end

    def parent
    self.wrap @node.parentNode
    end

    def child
    self.wrap @node.firstChild
    end

    def xpath
    if !@xpath
    factory = javax.xml.xpath.XPathFactory.newInstance()
    @xpath = factory.newXPath()
    end

    @xpath
    end

    def find(expr)
    nodelist = self.xpath.evaluate(expr, @node, javax.xml.xpath.XPathConstants::NODESET)

    ret = []
    for i in 0 .. (nodelist.length - 1)
    ret.push(WrapNode.new(nodelist.item(i)))
    end
    ret
    end
    end

    class WrapDocument < WrapNode
    def initialize(e)
    super(e)
    end

    def root
    self.wrap(@node.documentElement)
    end
    end

    # 指定されたファイルをopenしてXML文書を返す
    #
    # fn_in::
    # ファイル名。nilの場合、stdinを適用
    def build_doc(fn_in)
    st = java.lang.System.in
    if fn_in
    st = java.io.FileInputStream.new(fn_in)
    end

    factory = javax.xml.parsers.DocumentBuilderFactory.newInstance()
    builder = factory.newDocumentBuilder()
    WrapDocument.new(builder.parse(st))
    end

    doc = build_doc("foo.xml")
    doc.find("//bar").each { |e|
    puts e["name"]
    }