Rjb 1.0.7 primitive_conversion を導入してみました。

これまでのあらすじ

Java ライブラリ GeoToolsRuby から利用するライブラリである、「geotools.rb, for CRuby + JRuby」を作成していたところ、Rjb と JRuby ではプリミティブ型クラスの変換方式が異なることが分かりました。Rjb の作者である arton さんが本ブログをごらんになり、

Rjb::primitive_conversionというモジュールの擬似属性を追加。
Rjb::primitive_conversion = true を実行すると、以降、プリミティブ型クラスのオブジェクトが戻された場合、Rubyのネイティブ型に変換します。

http://arton.no-ip.info/diary/20070915.html#p01

とあるとおり、Rjb に JRuby 的なプリミティブ型クラス変換を導入する疑似属性 Rjb::primitive_conversion を追加した Rjb 1.0.7 をリリースしてくださいました。

本エントリの要約

1.0.7 対応をさせた geotools.rb を実行したところ、プリミティブ型クラス java.lang.Long の変換ができずにエラーとなっています。
以下は、詳細の説明です。

Rjb 1.0.7 導入

RubyGems から Rjb 1.0.7 を導入してみたところ、まったく問題なく導入できました(Mac OS X 10.4.10 + MacPorts):

hfu:~$ java -version
java version "1.5.0_07"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_07-164)
Java HotSpot(TM) Client VM (build 1.5.0_07-87, mixed mode, sharing)
hfu:~$ export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home 
hfu:~$ sudo gem update rjb
Password:
Updating installed gems...
Attempting remote update of rjb
Select which gem to install for your platform (i686-darwin8.9.1)
 1. rjb 1.0.7 (ruby)
 2. rjb 1.0.7 (mswin32)
 3. rjb 1.0.6 (mswin32)
 4. rjb 1.0.6 (ruby)
 5. Skip this gem
 6. Cancel installation
> 1
Building native extensions.  This could take a while...
Successfully installed rjb-1.0.7
Gems: [rjb] updated
hfu:~$ 

テストも問題なくパスしています:

hfu:/opt/local/lib/ruby/gems/1.8/gems/rjb-1.0.7/test$ ruby test.rb 
start RJB(1.0.7) test
Loaded suite test
Started
..............................
Finished in 0.546908 seconds.

30 tests, 113 assertions, 0 failures, 0 errors

geotools.rb 改造

Shapefile データについて繰り返す部分をメソッド iterate として括りだし、Java ブリッジの実装を見てそのメソッドの実装を切り替えるという考えで、geotools.rb を改造しました。具体的には以下のようにしています:

class Reader
  if Tools::IMPLEMENTATION == 'java'
    def iterate # JRuby 用実装
    end
  else
    begin
      Rjb::primitive_conversion = true
      def iterate # Rjb 1.0.7 用実装
      end
    rescue
      def iterate # Rjb 1.0.6 以前用実装
    end
  end
end

プログラムの全文は本エントリの末尾に添付いたします。

改造した geotools.rb の実行結果

JRuby で改造版を実行したとき、以下のように過去の通りの実行結果となります:

hfu:~$ jruby geotools.rb
DEBUG: jruby mode
205.757 sec.

次に、CRuby で primitive_conversion = false となるようにした場合です:

hfu:~$ ruby geotools.rb # primitive_conversion の綴りを壊してわざと 1.0.6 以前用の実装を使っている
DEBUG: rjb conventional mode
getting the_geom com.vividsolutions.jts.geom.MultiLineString
getting f_code java.lang.String
getting f_code_des java.lang.String
getting acc java.lang.Long
getting acc_descri java.lang.String
getting exs java.lang.Long
getting exs_descri java.lang.String
getting med java.lang.Long
getting med_descri java.lang.String
getting rst java.lang.Long
getting rst_descri java.lang.String
getting rsu java.lang.Long
getting rsu_descri java.lang.String
getting rtt java.lang.Long
getting rtt_descri java.lang.String
getting soc java.lang.String
getting tuc java.lang.Long
getting tuc_descri java.lang.String
getting fco java.lang.Long
getting fco_descri java.lang.String
getting the_geom com.vividsolutions.jts.geom.MultiLineString
getting f_code java.lang.String
...

「getting ...」というデバッグメッセージを消去して同じようにプログラムを実行すると、以下のように正常に終了します:

hfu:~$ ruby geotools.rb
DEBUG: rjb conventional mode
205.987645 sec.

しかし、CRuby で primitive_conversion = true としている場合には、以下のようにエラー終了してしまいます。

hfu:~$ ruby geotools.rb
DEBUG: rjb primitive_conversion mode
getting the_geom
getting f_code
getting f_code_des
getting acc
geotools.rb:54:in `method_missing': no convertor defined(1) (RuntimeError)
        from geotools.rb:54:in `iterate'
        from geotools.rb:52:in `times'
        from geotools.rb:52:in `iterate'
        from geotools.rb:95:in `foreach'
        from geotools.rb:229
        from geotools.rb:123:in `open'
        from geotools.rb:228

属性 acc の変換に失敗しているようです。primitive_conversion = false としたときの標準出力から、属性 acc の型は java.lang.Long であることが分かっていますので、java.lang.Long の変換に失敗しているのではないかと予想できます。
これについて、Rjb 1.0.7 で配布されている rjb.c の 527 行目から 544 行目を見ると、

	    switch (*(jpcvt[i].to_prim_method))
	    {
	    case 'i':
		jv.i = (*jenv)->CallIntMethod(jenv, o, jpcvt[i].to_prim_id);
		break;
	    case 'b':
		jv.z = (*jenv)->CallBooleanMethod(jenv, o, jpcvt[i].to_prim_id);
		break;
	    case 'd':
		jv.d = (*jenv)->CallDoubleMethod(jenv, o, jpcvt[i].to_prim_id);
		break;
	    case 'c':
		jv.c = (*jenv)->CallCharMethod(jenv, o, jpcvt[i].to_prim_id);
		break;
	    default:
		rb_raise(rb_eRuntimeError, "no convertor defined(%d)", i);
		break;
	    }

となっています。java.lang.Long に対応する case がないために、ここで rb_raise が呼び出されてしまっているようです。
rjb.c のこのあたりの処理で、java.lang.Long も変換するように修正していただけると、うまくいくのではないかと思っています。

現時点の geotools.rb の全文

# this code is under development and subject to major change.
require 'iconv'

module Geo
  # Geo::Tools module, to include nesessary classes from Geotools
  module Tools
    QUALIFIED_NAMES = %w{java.lang.String java.lang.Integer java.lang.Double java.lang.Long java.io.File org.geotools.data.shapefile.ShapefileDataStore org.geotools.feature.AttributeTypeFactory org.geotools.feature.FeatureTypeBuilder org.geotools.feature.type.GeometricAttributeType com.vividsolutions.jts.io.WKTReader org.geotools.referencing.crs.EPSGCRSAuthorityFactory org.geotools.referencing.operation.DefaultCoordinateOperationFactory org.geotools.geometry.DirectPosition2D}
    begin
      require 'rjb'
      QUALIFIED_NAMES.each do |qn|
        sn = qn.split('.').last
        module_eval "#{sn} = Rjb::import('#{qn}')"
      end
      IMPLEMENTATION = 'rjb'
    rescue LoadError
      require 'java'
      QUALIFIED_NAMES.each do |qn|
        include_class qn
      end
      IMPLEMENTATION = 'java'
    end
  end

  # Geo module variables
  @@wkt_reader = nil
  @@epsg_crs_authority_factory = nil

  # Geo module 'good-wrapper' / 'Grossklasstum' classes
  class Reader
    if Tools::IMPLEMENTATION == 'java'
      print "DEBUG: jruby mode\n"
      def iterate # same as the one for rjb 1.0.7 #TODO: DRY
        while(@iter.hasNext)
          feat = @iter.next
          attrs = {}
          feat.getNumberOfAttributes.times do |i|
            attr = feat.getAttribute(i)
            attrs[@attr_names[i]] = attr
          end
          attrs.delete('the_geom')
          yield feat.getDefaultGeometry, attrs
        end
      end
    else
      begin
        Rjb::primitive_conversion = true
        print "DEBUG: rjb primitive_conversion mode\n"
        def iterate # same as the one for jruby #TODO: DRY
          while(@iter.hasNext)
            feat = @iter.next
            attrs = {}
            feat.getNumberOfAttributes.times do |i|
              print "getting #{@attr_names[i]}\n"
              attr = feat.getAttribute(i)
              attrs[@attr_names[i]] = attr
            end
            attrs.delete('the_geom')
            yield feat.getDefaultGeometry, attrs
          end
        end
      rescue NoMethodError
        print "DEBUG: rjb conventional mode\n"
        def iterate
          while(@iter.hasNext)
            feat = @iter.next
            attrs = {}
            feat.getNumberOfAttributes.times do |i|
              attr = feat.getAttribute(i)
              print "getting #{@attr_names[i]} #{attr.getClass.getName}\n"
              if attr.getClass.equals(Tools::Integer)
                attr = attr.intValue
              elsif attr.getClass.equals(Tools::Double)
                attr = attr.doubleValue
              elsif attr.getClass.equals(Tools::String)
                if @sjis_workaround
                  attr = Iconv.conv('UTF-8', 'Shift_JIS', attr.getBytes('iso-8859-1'))
                else
                  attr = attr.toString
                end
              elsif attr.getClass.equals(Tools::Long)
                attr = attr.longValue
              end
              attrs[@attr_names[i]] = attr
            end
            attrs.delete('the_geom')
            yield feat.getDefaultGeometry, attrs
          end
        end
      end
    end

    ## TODO: implement attribute whitelist filtering (for better performance)
    def Reader::foreach(shapefile, sjis_workaround = false)
      r = Reader.new(shapefile, sjis_workaround)
      r.iterate do |geom, attrs|
        yield geom, attrs
      end
      r.close
    end

    def initialize(shapefile, sjis_workaround)
      if(Tools::IMPLEMENTATION == 'java' && sjis_workaround)
        raise "sjis_workaround for JRuby is not implemented."
      end
      store = Tools::ShapefileDataStore.new(Tools::File.new(shapefile).toURL)
      @iter = store.getFeatureSource.getFeatures.features
      feat_type = store.getFeatureSource.getSchema
      @attr_names = []
      feat_type.getAttributeCount.times do |i|
        @attr_names << feat_type.getAttributeType(i).getName
      end
      @sjis_workaround = sjis_workaround
    end

    def close
      @iter.close
    end
  end

  class Writer
    def Writer::open(shapefile)
      w = Writer.new(shapefile)
      yield w
      w.close
    end

    def initialize(shapefile)
      @shapefile = shapefile
      @writer = nil
      @first = true
    end
    
    def setup(geom, attrs)
      attrs.delete('the_geom')
      ftb = Tools::FeatureTypeBuilder.newInstance(@shapefile)
      attrs.each do |key, value|
        if value.methods.include?('_classname')
          attr_class = value.getClass
        elsif value.class == String
          attr_class = Tools::String
        elsif value.class == Fixnum
          attr_class = Tools::Integer
        elsif value.class == Float
          attr_class = Tools::Double
        else
          raise "attribute #{key} has unrecognizable class #{value.class}"
        end
        ftb.addType(Tools::AttributeTypeFactory.newAttributeType(key, attr_class))
      end
      if geom.class == String
        geom = import_wkt_geometry(geom)
      end
      ftb.setDefaultGeometry(Tools::GeometricAttributeType.new('the_geom', geom.getClass, true, nil, nil, nil))
      ft = ftb.getFeatureType
      store = Tools::ShapefileDataStore.new(Tools::File.new(@shapefile).toURL)
      store.createSchema(ft)
      @writer = store.getFeatureWriter(@shapefile, store.getFeatureSource(@shapefile).getTransaction)
      @first = false
    end
    private :setup

    def write(geom, attrs)
      setup(geom, attrs) if @first
      feat = @writer.next
      if geom.class == String
        geom = import_wkt_geometry(geom)
      end
      feat.setDefaultGeometry(geom)
      attrs.each do |key, value|
        feat.setAttribute(key, value)
      end
      @writer.write
    end

    def close
      @writer.close unless @writer == nil
    end
  end

  class Transform
    def initialize(src_crs, dst_crs)
      cof = Geo::Tools::DefaultCoordinateOperationFactory.new
      @co = cof.createOperation(src_crs, dst_crs)
      @mt = @co.getMathTransform
    end

    def transform(x, y) # z?
      r = Geo::Tools::DirectPosition2D.new
      @mt.transform(Geo::Tools::DirectPosition2D.new(x, y), r)
      return r.x, r.y
    end

    ## TODO: def transform(geom)
    ## TODO: accessor to @mt or @co
  end

  # Geo module convenient methods
  def Geo::import_wkt_geometry(wkt)
    @@wkt_reader = Geo::Tools::WKTReader.new if @@wkt_reader == nil
    @@wkt_reader.read(wkt)
  end

  def Geo::import_epsg_crs(epsg_code)
    @@epsg_crs_authority_factory = Geo::Tools::EPSGCRSAuthorityFactory.new if @@epsg_crs_authority_factory == nil
    if epsg_code.class == Fixnum
      return @@epsg_crs_authority_factory.createCoordinateReferenceSystem("EPSG:#{epsg_code}")
    elsif epsg_code.class == String
      return @@epsg_crs_authority_factory.createCoordinateReferenceSystem(epsg_code)
    else
      raise "Geo::import_epsg_crs: can not handle epsg_code = #{epsg_code}"
    end
  end

  def dms2dec(d, m, s)
    d + m / 60.0 + s / 3600.0
  end

  def dec2dms(dec)
    #TODO
    raise "not implemented."
  end
end

# ad hoc tests
if __FILE__ == $0
  ## TODO: better separate tests as unit tests.
  start_time = Time.now
  Geo::Writer.open('test.shp') do |w|
    Geo::Reader.foreach('transl_1_1.shp', false) do |geom, attrs|
      w.write(geom, attrs)
    end
  end
  print "#{Time.now - start_time} sec.\n"
end

ChangeLog その他

Sat Sep 15 07:31:14 CEST 2007: 明日日曜日づけでエントリ作成。