Rjb 1.0.7 primitive_conversion を導入してみました。
これまでのあらすじ
Java ライブラリ GeoTools を Ruby から利用するライブラリである、「geotools.rb, for CRuby + JRuby」を作成していたところ、Rjb と JRuby ではプリミティブ型クラスの変換方式が異なることが分かりました。Rjb の作者である arton さんが本ブログをごらんになり、
Rjb::primitive_conversionというモジュールの擬似属性を追加。
http://arton.no-ip.info/diary/20070915.html#p01
Rjb::primitive_conversion = true を実行すると、以降、プリミティブ型クラスのオブジェクトが戻された場合、Rubyのネイティブ型に変換します。
とあるとおり、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: 明日日曜日づけでエントリ作成。