jrubyからApache POIを使う超手抜きモジュール
rubyからExcelファイルを読み書きするのは、Spreadsheet(pure ruby)とかwin32oleを使う方法がある。
たまたま、とあるExcelテンプレートファイルを読んで、ごにょごにょ編集して、別ファイルに出力するという機会があった。
この処理をLinux上で動かしたかったため、Spreadsheetを試してみたが、運悪くそのテンプレートファイルには
シートの保護がなされていて、出力したExcelファイルがどうしても壊れてしまう。
このへんは、Spreadsheetが対応できていないようだ。
ほかに似たようなライブラリがないか探してみたが、どうにも見つからない。
仕方がないので、実績のあるApache POIをjrubyから使ってみることにした。
さすがにPOI。難なくやりたいことができてしまった。
それにしてもこのPOI、値をセットするときは型を気にする必要はないが、
取得するときはいちいちセルの型を調べないといけないのがちょっと面倒…
poi_workbook.rb:
require 'java' require './poi-3.7-20101029.jar' require './poi-ooxml-3.7-20101029.jar' java_import java.io.FileInputStream java_import java.io.FileOutputStream java_import org.apache.poi.ss.usermodel.WorkbookFactory java_import org.apache.poi.ss.usermodel.DataFormatter class Workbook def initialize(path) @fis = FileInputStream.new(path) @book = WorkbookFactory::create(@fis) end def self.open(path, &block) book = Workbook.new(path) return book unless block_given? yield book ensure book.close end def write(path) fos = FileOutputStream.new(path) @book.write(fos) ensure fos.close end def close @fis.close end def select_sheet_at(index) @sheet = @book.get_sheet_at(index) @sheet.set_force_formula_recalculation(true) end def []=(row_idx, col_idx, value) @sheet.create_row(row_idx) if @sheet.get_row(row_idx).nil? row = @sheet.get_row(row_idx) row.create_cell(col_idx) if row.get_cell(col_idx).nil? row.get_cell(col_idx).set_cell_value(value) end def [](row, col) return nil if (row = @sheet.get_row(row)).nil? or (cell = row.get_cell(col)).nil? return DataFormatter.new.format_cell_value(cell) end end if __FILE__ == $0 # sample usage Workbook.open('template.xls') do |book| book.select_sheet_at(0) book[0, 0] = book[0, 0] + ' new value' book.write('new.xls') end end
※細々としたセルの値に対応していません。
drubyで作る超適当なRackセッション
Rack::Session::Memcacheを参考にdrubyで同じようなことをやってみたら、意外と簡単にできてしまった。
あとは、セッション管理側に適切な排他制御を実装すれば、それなりに使えるかな。
セッションを保持するdrubyサーバ
drb_session_manager.rb:
require 'drb' class DRbSessionManager attr_reader :pool def initialize @pool = {} end def set(session_id, session) @pool[session_id] = session end def get(session_id) @pool[session_id] end end DRb.start_service('druby://localhost:11211', DRbSessionManager.new) sleep
drubyサーバにアクセスするRackのセッション
drbpool.rb:
require 'rack/session/abstract/id' require 'drb' module Rack module Session class DRbPool < Abstract::ID attr_reader :pool DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \ :namespace => 'rack:session', :druby_server => 'druby://localhost:11211' def initialize(app, options={}) super @pool = DRbObject.new_with_uri(@default_options[:druby_server]) end def generate_sid loop do sid = super break sid unless @pool.get(sid) end end def get_session(env, session_id) unless session_id and session = @pool.get(session_id) session_id, session = generate_sid, {} @pool.set(session_id, session) end return [session_id, session] end def set_session(env, session_id, new_session, options) @pool.set(session_id, new_session) return session_id end end end end
sinatraを使ってセッションのテスト
main.rb:
require 'sinatra' require './drbpool' use Rack::Session::DRbPool get('/') do session[:count] = (session[:count] || 0) + 1 "count: #{session[:count]}" end
実行してみる。
hiro@neptune% ruby drb_session_manager.rb & [1] 4144 hiro@neptune% ruby main.rb == Sinatra/1.1.2 has taken the stage on 4567 for development with backup from Thin >> Thin web server (v1.2.7 codename No Hup) >> Maximum connections set to 1024 >> Listening on 0.0.0.0:4567, CTRL+C to stop
別ターミナルで。
hiro@neptune% curl --cookie-jar cookie.txt http://localhost:4567 count: 1 hiro@neptune% cat cookie.txt # Netscape HTTP Cookie File # http://curl.haxx.se/rfc/cookie_spec.html # This file was generated by libcurl! Edit at your own risk. #HttpOnly_localhost FALSE / FALSE 0 rack.session d0673073b2f257040f22a0757d3e67f8 hiro@neptune% curl --cookie rack.session=d0673073b2f257040f22a0757d3e67f8 http://localhost:4567 count: 2 hiro@neptune% curl --cookie rack.session=d0673073b2f257040f22a0757d3e67f8 http://localhost:4567 count: 3
DRbSessionManagerの中身を確認してみる。
hiro@neptune% irb irb(main):001:0> require 'drb' => true irb(main):002:0> d = DRbObject.new_with_uri('druby://localhost:11211') => #<DRbSessionManager:0x10164bc0> irb(main):003:0> d.pool => {"d0673073b2f257040f22a0757d3e67f8"=>{:count=>3}} irb(main):004:0>
マルチプロセスとマルチスレッド
unicornを使ったときのセッション管理をいろいろ調べていたが、
よくよく考えるとunicornはマルチプロセスなわけで…。
じゃ、Rack::Session::Poolは使えないのかと思って、
起動プロセス数を増やして試してみたら案の定、うまく動かなかった。
プロセスが違うんだから、確かに動くわけないよなぁ。
今まで動いていたように見えたのは、たまたま同じプロセスにリクエストが飛んでただけだったか。
ということで、マルチプロセスなWEBサーバでは、Rack::Session::Memcacheとか
使うことになるのかな。それか、druby使ってセッション管理してしまうとか。
普段は、Javaばっかりなものだから、すっかりマルチスレッドな考え方が染みついてる。
頭堅くなってるわ。
Rackとミドルウェア
ここしばらく、Rackのソースと格闘している。
インスタンス化されたRack::Session::Poolはどのようにして参照するのかと思い、
server.rbとかbuilder.rbらへんをつらつらと読んでいたが、
おそらくアプリケーション側からは参照できないんじゃないかという気になってきた。
config.ruで定義されたミドルウェアは、Rack::Builder#to_appでインスタンス生成されるが、
runしているクラスがアプリケーションとなり、順次ミドルウェアのコンストラクタに渡っているっぽいから、
直接ミドルウェアのインスタンスをを参照する手段ってないような…
それにしても、処理の流れが難しいなぁ。
jrubyからJavaのクラスを扱う
jrubyでJavaのクラスを扱うときはどうしたらよいのか?
最初、ドキュメントがぱっと見つけられなくてちょっと困った。
https://github.com/jruby/jruby/wiki/CallingJavaFromJRuby
まず最初のおまじない、require 'java' しておく。
・Jarファイルの読み込み
requireを使えばよい。直接ファイルパスを書くか、グローバル変数$LOAD_PATH
またはシェルの環境変数CLASSPATHにJarファイルを置いて、ファイル名のみ指定する。
・classファイルの読み込み
コマンドラインまたは環境変数CLASSPATHでクラスパスを指定する。このあたりはjavaと同じ。
$ jruby -J-cp [クラスパス] ... または $ export CLASSPATH=/path/to
グローバル変数 $CLASSPATH も使える。この変数は上記で指定された内容と関係ない(初期状態は空の配列)。
$CLASSPATH << '/path/to'
クラス名だけで参照したい場合はimportする。
importもjava_importも同じだが、jrubyの新しいバージョンではjava_importがおすすめのよう。
javaやorgで始まるパッケージなどは、文字列にしなくてもよい(このへんでちょっとはまった)。
import 'foo.bar.Baz' または java_import 'foo.bar.Baz'
試してみる。
hiro@Mac-mini% find ./classes ./lib -type f ./classes/foo/bar/Baz.class ./lib/servlet-api.jar
hiro@Mac-mini% cat a.rb require 'pp' require 'java' require '/Users/hiro/devel/ruby/sandbox/lib/servlet-api.jar' $CLASSPATH << './classes' p java.lang.System p javax.servlet.Servlet p Java::foo.bar.Baz # foo.bar.Bazだとエラーになる java_import java.lang.System java_import javax.servlet.Servlet java_import 'foo.bar.Baz' p System p Servlet p Baz p Java.class pp Java.constants
hiro@Mac-mini% jruby a.rb Java::JavaLang::System Java::JavaxServlet::Servlet Java::FooBar::Baz Java::JavaLang::System Java::JavaxServlet::Servlet Java::FooBar::Baz Module ["JavaLang", "JavaProxyClass", "JavaNio", "JavaConstructor", "JavaField", "Javax", "JavaProxyMethod", ...
rubyとjrubyのエンコーディング
hiro@Mac-mini% cat a.rb p File.read('foo.txt').encoding hiro@Mac-mini% cat foo.txt foo hiro@Mac-mini% jruby --1.9 -E UTF-8 a.rb #<Encoding:ASCII-8BIT> hiro@Mac-mini% ruby -E UTF-8 a.rb #<Encoding:UTF-8>
rubyとjrubyって挙動はいっしょと思っていたけど、エンコーディングで微妙に動作が違う。これって仕様なんかなぁ。
REXMLでSJISのXMLを解析した文字列と、YAMLから読み込んだ文字列をhamlのテンプレートを使って出力する、という処理をしていたが、
Rubyだとそれほどエンコーディングを意識しなくてすんだのが、そのままjrubyで動かすと途端にエラーの嵐が…
すべてUTF-8に固めて出力を試みたが、最後のhamlでうまくUTF-8になってくれなくて、あえなく断念。
結局、最後はascii-8bitに統一して出力したらうまくいったけど、う〜ん、どうも気持ち悪い。