読者です 読者をやめる 読者になる 読者になる
無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

assertEqualsを作るスクリプト

Ruby Java クラス解析機

javaclassのサンプルその2。指定したJavaBeanのassertEqualsをさくっと作るスクリプトです。
equals()が実装されていなくて、assertEquals(Object, Object)で単純に比較できないJavaBeanの場合、フィールドを1つづつ取り出して評価する必要があって面倒ですよね!そんな困ったJavaBeanがたくさんあるときに使える(かもしれない)スクリプトです。

例えば、次のようなクラスを対象に実行すると、

package com.example;

public class Kitten {
    private int age;
    private String name;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

以下のassertEquals()メソッドを出力します。

/**
 * {@link com.example.Kitten} が同一であることを評価する。
 *
 * @param expected 期待値
 * @param actual 実際の値
 */
public static final void assertEquals( 
    com.example.Kitten expected, 
    com.example.Kitten actual ) {
    assertEquals( expected.getAge(), actual.getAge() );
    assertEquals( expected.getName(), actual.getName() );
}

まぁ、これだけなら、リフレクションで「get.*」と「is.*」を探してきて比較する汎用assertEqualsメソッドとか用意すればいいんでは、という話もあります。ただ、クラスごとに評価内容をカスタマイズする必要がある場合(kittenのnameは比較しない、とか)などは、個別にassertEquals()を用意しておいた方がやりやすいかと。

使い方

以下のコマンドで

  • jarファイル内の正規表現にマッチするクラスのassertEqualsメソッドを出力します。
  • 正規表現が省略された場合、すべてのクラスを対象とします。
./create_assert.rb <jarファイル> [<対象クラスを示す正規表現>]

※なお、例によって、javaclassrubyzipに依存しているので、あらかじめインストールしておく必要があります。インストール手順は、こちらを参照

実装

create_assert.rbの実装は以下。

#!/usr/bin/ruby

# jar内のクラスファイルのassertEquals()を生成する。
#
# ./create_assert.rb <jarファイル> [<対象クラスを示す正規表現>]
#

require 'rubygems'
require "javaclass"
require "zip/zip"
require "kconv"
require "erb"


TEMPLATE = <<-ERB
% name = jc.name
    /**
     * {@link <%= name.gsub(/\\$/, ".") %>} が同一であることを評価する。
     *
     * @param expected 期待値
     * @param actual 実際の値
     */
    public static final void assertEquals( 
        <%= name.gsub(/\\$/, ".") %> expected, 
        <%= name.gsub(/\\$/, ".") %> actual ) {
% if jc.super_class != nil && jc.super_class != "java.lang.Object"
%  super_class = jc.super_class.gsub(/\\$/, ".")
        assertEquals( (<%= super_class %>) expected, (<%= super_class %>) actual );
% end   
% jc.methods.each { |m|
%   next if m.deprecated?
%   next if m.access_flag.on? JavaClass::MethodAccessFlag::ACC_STATIC
%   next if m.access_flag.on? JavaClass::MethodAccessFlag::ACC_PRIVATE
%   if m.name =~ /^get(\\S+)/ || m.name =~ /^is(\\S)+/
%     sname = $1
%     d = JavaClass::Converters.convert_method_descriptor( m.descriptor )
%     next if d[:args].length > 0
%     if d[:return] =~ /List$/ || d[:return] =~ /.*\\[\\]/ || d[:return] =~ /Map$/
        <%= d[:return] %> expected<%= sname%> = expected.<%= m.name %>();
        <%= d[:return] %> actual<%= sname%> = actual.<%= m.name %>();
        if ( expected<%= sname%> != null ) {
%       if d[:return] =~ /List$/ 
            assertEquals( expected<%= sname%>.size(), actual<%= sname%>.size() );
            for ( int i = 0; i < expected<%= sname%>.size(); i++ ) {
                assertEquals( expected<%= sname%>.get(i), actual<%= sname%>.get(i) );
            }
%       elsif d[:return] =~ /.*\\[\\]/
            assertEquals( expected<%= sname%>.length, actual<%= sname%>.length );
            for ( int i = 0; i < expected<%= sname%>.length; i++ ) {
                assertEquals( expected<%= sname%>[i], actual<%= sname%>[i] );
            }
%       else
            assertEquals( expected<%= sname%>.size(), actual<%= sname%>.size() );
            for ( Object key : expected<%= sname%>.keySet() ) {
                assertEquals( expected<%= sname%>.get(key), actual<%= sname%>.get(key) );
            }
%       end
        } else {
            assertEquals( expected<%= sname%>, actual<%= sname%> );
        }
%     else
        assertEquals( expected.<%= m.name %>(), actual.<%= m.name %>() );
%     end
%   end
% } 
    }
ERB

# ZipInputStreamにはgetcが実装されていないので、追加する。
module Zip
  class ZipInputStream
    def getc
      read(1)[0]
    end
  end
end

# Zipエントリ内のクラス一覧を列挙して解析する。
def each_class ( zip_file, &block ) 
  Zip::ZipFile.foreach(zip_file) {|entry|
    next unless entry.file?
    next unless entry.name =~ /.*\.class$/
    entry.get_input_stream {|io|
      jc = JavaClass.from io
      block.call( jc ) if block_given?
    }
  }
end

erb = ERB.new(TEMPLATE, nil, "%" )
r = Regexp.compile(ARGV[1]) if ( ARGV.length > 1 ) 

each_class( ARGV[0] ) {|jc|
  
  next if r != nil && !r.match(jc.name)
    
  # enum, annotationは除外
  next if jc.access_flag.on? JavaClass::ClassAccessFlag::ACC_ENUM
  next if jc.access_flag.on? JavaClass::ClassAccessFlag::ACC_ANNOTATION
  
  # deprecatedなクラスも除外
  next if jc.deprecated?
  puts erb.result(binding).tosjis
}