Nokogiri をWindowsに入れる場合のエラーについて

環境

症状

Nokogiri のgemをインストールしようとすると、

gem install nokogiri

以下のように変換できない文字があってエラーが発生する。

Fetching: nokogiri-1.5.5-x86-mingw32.gem (100%)
Successfully installed nokogiri-1.5.5-x86-mingw32
1 gem installed
Installing ri documentation for nokogiri-1.5.5-x86-mingw32...
unable to convert "\xE3" to UTF-8 in conversion from ASCII-8BIT to UTF-8 to Windows-31J for CHANGELOG.ja.rdoc, skipping
unable to convert "\xE8" to UTF-8 in conversion from ASCII-8BIT to UTF-8 to Windows-31J for CHANGELOG.rdoc, skipping
unable to convert "\xE9" to UTF-8 in conversion from ASCII-8BIT to UTF-8 to Windows-31J for README.ja.rdoc, skipping
unable to convert "\xE2" to UTF-8 in conversion from ASCII-8BIT to UTF-8 to Windows-31J for ext/nokogiri/xml_node_set.c, skipping
Installing RDoc documentation for nokogiri-1.5.5-x86-mingw32...
unable to convert "\xE3" to UTF-8 in conversion from ASCII-8BIT to UTF-8 to Windows-31J for CHANGELOG.ja.rdoc, skipping
unable to convert "\xE8" to UTF-8 in conversion from ASCII-8BIT to UTF-8 to Windows-31J for CHANGELOG.rdoc, skipping
unable to convert "\xE9" to UTF-8 in conversion from ASCII-8BIT to UTF-8 to Windows-31J for README.ja.rdoc, skipping
unable to convert "\xE2" to UTF-8 in conversion from ASCII-8BIT to UTF-8 to Windows-31J for ext/nokogiri/xml_node_set.c, skipping

対処

gem コマンドの実行前に、コマンドプロンプトのコードページをUTF-8に変更しておく。

chcp 65001
gem install nokogiri

これで、上のようにエラーが出ていたファイルも正しく変換される。ファイルがUTF-8になってしまうがたぶんHTMLだけなので問題ない。

プロキシがある場合のツールの設定いろいろ その2

プロキシがある場合のツール等の設定いろいろ - penultimate diary の続き。各ツールでのWindows環境における認証つきプロキシの通り方。

npm

コマンドプロンプトで以下のようにする。

npm config set proxy http://<username>:<password>@<proxy-host>:<proxy-port>
npm config set https-proxy http://<username>:<password>@<proxy-host>:<proxy-port>

一応補足すると username, password はプロキシのユーザ名・パスワード。proxy-host と proxy-port はプロキシサーバのホスト名とポート番号。
上では HTTP も HTTPS も同じプロキシを設定しているが、違うのならURLを変更すること。

また、私の環境だけかもしれないが、プロキシ越しだと証明書のエラーにより通信できないため、以下のコマンドも実行する必要があった。ただ、これを行うと接続先が本当に本物の npmjs.org サイトかどうか検証しなくなるということなので、良く考えること。

npm config set strict-ssl false

認証情報についての補足

上記の npm のようにプロキシをURL形式で指定する場合、URLなので認証情報部分(上記の username, password)などはURLエンコードして設定するのが正しいはず。

例えば npm では、ユーザ名 user、パスワード P@ssword の場合、以下のようにすれば正しく設定される。

npm config set proxy http://user:P%40ssword@proxy.example.com:8080

ところが gem というか rubyURI ライブラリだか Net::HTTP ライブラリだかは認証情報部分をきちんとデコードしてくれないので、

set http_proxy=http://user:P%40ssword@proxy.example.com:8080

は、パスワードがそのまま "P%40ssword" で送られてしまい認証に失敗するし、かといって

set http_proxy=http://proxy.example.com:8080
set http_proxy_user=user
set http_proxy_pass=P@ssword

のように指定してみても、↓こんな風に

ERROR:  While executing gem ... (URI::InvalidComponentError)
    bad component(expected user component): P@ssword

URLの認証情報部分として形式がおかしいとかエラーが出て通信できない。つまり、認証情報にアットマークを含むと八方塞がり。

しょうがないので Rubyのホームディレクトリ\lib\ruby\1.9.1\uri\generic.rb の438-441行目を一時的にコメントアウトして

#      if parser.regexp[:USERINFO] !~ v
#        raise InvalidComponentError,
#          "bad component(expected user component): #{v}"
#      end

とした上でURLエンコードせず http_proxy_pass=P@ssword で設定したら上手くいくようになった。

Javaでパスワードのハッシュ化: PBKDF2

エフセキュアブログ : 再録:パスワードは本当にSHA-1+saltで十分だと思いますか? に望ましいパスワードのハッシュ化方法について具体的に載っていたので、Javaでどんな感じになるのか確認してみる。

使うのは現在一応安全だと認められているっぽいもので、Oracle JDKにも標準で含まれている PBKDF2 *1 という方式。Wikipedia によると色々なプロダクトでも使われているようだ。

テスト

package example;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class PBKDF2 {

    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException {
        String p1 = args[0];
        String p2 = args[1];
        printfln("password 1: %s", p1);
        printfln("password 2: %s", p2);

        byte[] salt = createSalt();
        printfln("salt: %s", Arrays.toString(salt));

        byte[] d1 = pbkdf2(p1.toCharArray(), salt);
        byte[] d2 = pbkdf2(p2.toCharArray(), salt);
        printfln("derived 1: %s", Arrays.toString(d1));
        printfln("derived 2: %s", Arrays.toString(d2));
    }

    // ランダムなsaltを生成
    static byte[] createSalt() throws NoSuchAlgorithmException {
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        byte[] salt = new byte[32];
        random.nextBytes(salt);
        return salt;
    }

    static byte[] pbkdf2(char[] password, byte[] salt) throws InvalidKeySpecException, NoSuchAlgorithmException {
        // Oracle JDKに含まれる PBKDF2 の唯一の実装
        SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        // 繰り返し回数:10000回、結果の長さ:256bit
        KeySpec ks = new PBEKeySpec(password, salt, 10000, 256);
        SecretKey s = f.generateSecret(ks);
        return s.getEncoded();
    }

    // 補助
    private static void printfln(String format, Object ... args) {
        System.out.printf(format + "%n", args);
    }
}

確認(Maven2使用)

>mvn -q exec:java -Dexec.mainClass="example.PBKDF2" -Dexec.args="a a"
password 1: a
password 2: a
salt: [71, 25, 97, -89, -30, 77, 37, 19, -14, 52, 20, -46, 98, 36, -41, 54, 127,
 -76, -53, 97, 23, 53, 107, -67, -36, -120, 67, -108, 122, -106, -63, -64]
derived 1: [79, 18, 24, 95, 116, -121, -70, -72, 73, -68, -115, -48, -104, 119,
7, 56, 84, 98, -34, 90, 108, -31, 58, -107, 119, 105, 84, 45, -67, -6, -19, 4]
derived 2: [79, 18, 24, 95, 116, -121, -70, -72, 73, -68, -115, -48, -104, 119,
7, 56, 84, 98, -34, 90, 108, -31, 58, -107, 119, 105, 84, 45, -67, -6, -19, 4]
>mvn -q exec:java -Dexec.mainClass="example.PBKDF2" -Dexec.args="a b"
password 1: a
password 2: b
salt: [-30, 13, 106, -63, 31, -92, -61, 49, -29, -72, 48, 0, -96, -6, -124, 57,
-116, -78, 64, -23, -76, -68, -117, 29, 17, -119, 4, -85, -107, 29, 88, 60]
derived 1: [-121, -57, 70, -10, 111, 22, 79, -48, -113, -27, 85, 49, 80, 112, -2
7, 60, -114, 98, -88, -90, 117, -75, 28, 4, -10, 82, -68, -7, 31, 46, 20, -89]
derived 2: [40, -15, -42, -44, 125, -128, 122, -122, 81, -63, -31, -25, -16, 14,
 -105, -90, -48, -17, 54, -92, -59, 95, 28, 50, 88, 55, -41, 66, 63, -68, -89, -
109]

他、調査の過程で知ったこといろいろ。

その他のハッシュ方式について

現在では、 memory-bound なアルゴリズムを使った scrypt の方が、CPU-Boundなアルゴリズムである PBKDF2 よりも総当り攻撃に強いとされているらしい。

…と、いろんなところでそう書かれているけどどのくらい検証されているのかは不明。Java版の実装も GitHub - wg/scrypt: Java implementation of scrypt にあるようだけど、正しく実装されているか確認する時間がない…。

既存パスワードのハッシュ方式の変更方法

将来、CPU/GPUの性能が向上して、そろそろハッシュ方式を変更しなければならなくなったけど、元のパスワードは保管していないので再計算なんてできない! どうしよう?

答え:「ユーザのパスワードが古い形式で保管されていたら古い形式で認証し、ログイン成功したら、新しいハッシュ方式で計算し直した結果で上書き」という機能を仕込んでおけば良い。ある程度時間が必要だけど。

ハッシュ化されたパスワードの保管方法

計算した結果をDB等にどういう形式で保管するか。

考慮する点:

  • saltは後で必要なので一緒に保管する必要がある
  • 上で述べたように後でハッシュ化の方式を変更することを考えると、どの方式でハッシュ化されたかについての情報も含めた方が良いかも?*2

ひとつの候補として、UNIX関係で広く使われている Modular Crypt Format という形式があり、PythonのPBKDF2ライブラリがこれに近い形式の出力文字列を定めているので、この形式に従うのも良いかもしれない。

*1:略語が覚えにくいので、Password-Based Key Derivation Function: パスワードベースの鍵導出関数、と元の言葉を覚えたほうが早い

*2:漏洩した場合に方式だけ隠していてもあんまり意味がなさそうなので、含めても問題ないよね? たぶん…

JUnit4のRunner概説

Runner が何かについては省略。テストの実行のしかたを決めるものと考えればよい。

Runner の指定方法

テストを実行するための Runner を指定するには、基本的にテストクラスに @RunWithアノテーションを設定し、そのパラメータに使いたい Runner のクラスを指定する。

例えば、以下のようにすると Parameterized が使われる。

import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class MyTestClass {
...

@Ignore

@RunWith 以外で Runner を指定できるケースとして @Ignoreアノテーションがある。

これを指定すると IgnoredClassRunner という Runner によりクラス全体がテスト対象から除外される。

import org.junit.Ignore;

@Ignore
public class MyTestClass {
    @Test
    public void このテストは無視されます() {}
}

標準の Runner

テストクラスに @Ignore も @RunWith も指定しないと*1標準の Runner である BlockJUnit4ClassRunner が使用される。

@RunWith(BlockJUnit4ClassRunner.class) 

↑でも同じだが、このように明示的に指定する場合は JUnit4 クラスを使うことが推奨されている*2

@RunWith(JUnit4.class)

その他の Runner

@RunWith に指定できる JUnitに組み込みの Runner には以下のものがある。

Suite

同時に設定する @SuiteClasses に指定されたクラス全てに対して再帰的にテストを実施する。

例えば、以下のように設定したクラス MyTestSuite に対してテストを実行すると、MyTest1, MyTest2 それぞれに対してテストが実行される。それぞれに対するテストは直接実行した場合と同じように行われるので、@Ignore を指定していれば無視されるし、@RunWith(Suite.class) を指定すれば更に再帰的に実施される。

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Suite.class)
@SuiteClasses(MyTest1.class, MyTest2.class)
public class MyTestSuite {
    // 中身は別に要らない
}

この Suite と以下に出てくる Categories, Enclosed は「他のクラスをテスト対象として指定するもの」で、自クラスはテスト対象にならない。なので自クラス内に @Test、@Before、@After アノテーションが付いたメソッドがあっても実行されない(@BeforeClass, @AfterClass は実行される)。

Categories

Suite のサブクラス。@SuiteClasses に指定されたクラス全てに対して再帰的にテストを実施するが、その際に @Category アノテーションによるフィルダリングを行う。

用例は CategoriesJavadoc にある。

なお、@Category に指定するクラスは単なるマーカーでありクラスの内容は全く無関係であるため、以下のように既存の適当なクラスを指定しても問題なかったりする。

import org.junit.Test;
import org.junit.experimental.categories.Categories;
import org.junit.experimental.categories.Categories.IncludeCategory;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Categories.class)
@SuiteClasses({MyTestSuite.Test1.class, MyTestSuite.Test2.class})
// @Category(Integer.class) のテストのみ実行
@IncludeCategory(Integer.class)
public class MyTestSuite {

    @Category(Integer.class)
    public static class Test1 {
        @Test public void test1() {}
    }

    @Category(Long.class)
    public static class Test2 {
        @Test public void test2() {}
    }
}
Enclosed

これも Suite のサブクラスだが、テスト対象のクラスを @SuiteClass で指定する必要が無く、自動的にインナークラス(Class#getClasses() で取得できるクラス)全てをテスト対象としてくれるという点のみ異なる。

例えば、以下のようにするとクラス Test1, Test2 が両方ともテスト対象となる。

import org.junit.experimental.runners.Enclosed;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(Enclosed.class)
public class MyTestSuite {

    public static class Test1 {
        @Test public void test1() {}
    }

    public static class Test2 {
        @Test public void test2() {}
    }
}

このときの実行のされ方は Suite と同じなので、@Before や @After 等も Test1, Test2 のそれぞれに指定したものが使用され、MyTestSuite に指定しても実行されない。

ただ、@BeforeClass, @AfterClass は実行されるので、共通の初期化を行うことは一応可能だ。

Parameterized

ParameterizedクラスのJavadocにあるように、パラメータリストを返すメソッドを作成し@Parameters アノテーションをつけると、そのパラメータ全てに対して @Test をつけたテストが繰り返し実行される。

これも Suite のサブクラスだが、上の3つとは異なりテスト対象はあくまでも自クラスになるため @Before や @After も各テストの実行前後に実行される。

Theories

大量の条件があって、それら全ての組み合わせに対してテストが通ることを確認したい場合に使える? Runner。

例えば、日本人の伝統的な名前であれば true を返す NameChecker.isJapaneseClassicName(String) というメソッドがあり、これが色々な名字・名前の組み合わせについて true と判定してくれるかどうかテストしたいとする。その場合、以下のようなテストになるかもしれない。

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

import java.util.Arrays;
import java.util.List;

import org.junit.experimental.theories.ParameterSignature;
import org.junit.experimental.theories.ParameterSupplier;
import org.junit.experimental.theories.ParametersSuppliedBy;
import org.junit.experimental.theories.PotentialAssignment;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

@RunWith(Theories.class)
public class TheoryTest {

    // テストパラメータ(名字)の一覧を返すクラス
    public static class LastNameSupplier extends ParameterSupplier {
        @Override
        public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
            return Arrays.asList(new PotentialAssignment[]{
                    PotentialAssignment.forValue("名字1", "山田"),
                    PotentialAssignment.forValue("名字2", "田中"),
                    PotentialAssignment.forValue("名字3", "佐藤"),
                    PotentialAssignment.forValue("名字4", "鈴木"),
            });
        }
    };

    // テストパラメータ(名前)の一覧を返すクラス
    public static class FirstNameSupplier extends ParameterSupplier {
        @Override
        public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
            return Arrays.asList(new PotentialAssignment[]{
                    PotentialAssignment.forValue("名前1", "太郎"),
                    PotentialAssignment.forValue("名前2", "花子"),
            });
        }
    };

    @Theory
    public void テスト(
            @ParametersSuppliedBy(FirstNameSupplier.class) String firstName,
            @ParametersSuppliedBy(LastNameSupplier.class) String lastName) throws Exception {
        System.out.println("checking name: " + lastName + firstName);
        assertThat(
            NameChecker.isJapaneseClassicName(lastName + firstName),
            is(true));
    }
}

必要な作業は、

  • テストしたいパラメータ値を PotentialAssignment として返す ParameterSupplier のサブクラスを作成する
  • テストメソッドには @Test の代わりに @Theory を付ける
  • テストメソッドのパラメータに @ParametersSuppliedBy(作成したParameterSupplierのサブクラス) を指定する

となる。

これを実行すると、以下のように全てのパラメータの組み合わせについてテストが実施される。

checking name: 山田太郎
checking name: 田中太郎
checking name: 佐藤太郎
checking name: 鈴木太郎
checking name: 山田花子
checking name: 田中花子
checking name: 佐藤花子
checking name: 鈴木花子

PotentialAssignment.forValue() の第一引数はパラメータに対するラベルになり、例えば「鈴木花子」でテストに失敗すると以下のように表示される。

org.junit.experimental.theories.internal.ParameterizedAssertionError: テスト(名前2, 名字4)
...

テストパラメータの指定は他に @DataPoint, @DataPoints で行う方法もあるが、こちらは解説しているサイトが結構ありそうなので省略。

Theories は Parameterized と使いどころが似ているが、「大量のパラメータ全てに対して同じ結果となる理論(theory)=テスト対象ロジック」を確認する場合に使うもので、「色々なパラメータが様々な結果となる」場合は Parameterized の方が使いやすいと思われる。*3

*1:厳密には suite() メソッドが無いとか他にも条件がある

*2:今はこの2つは全く同じだが、将来的に中身が変わり JUnit4 クラスが標準になる可能性があるため

*3:Assume を使うことで条件付の Theory 確認もできるが、そこまでいくとテスト自体にロジックが入りすぎという気がする

JAXBニッチ技特集: XMLを属性に基づいて特定のサブクラスに非整列化(unmarshal)する

元ネタはStackOverflowのこちらの質問。

inheritance - Java/JAXB: Unmarshall Xml to specific subclass based on an attribute - Stack Overflow

上記の例ではクラス名そのものを属性値として設定しているが、そうではなく「コード値」的な属性でサブクラスが指定される場合にはどうすれば良いか?

どういう場合かというと、例えば以下のようなXMLを非整列化したいとする。

<commandList>
  <command code="C" data="新規作成データ" />
  <command code="D" id="1" />
</commandList>

これはデータ操作を 要素で指定するもので、操作の種類は code 属性で決まる。

  • C(CREATE)なら data 属性の値を使って新規レコードを作成する。
  • D(DELETE)なら id 属性に指定したIDのレコードを削除する。

なので、command 要素は、全ての考えられる属性を持った単一クラスに非整列化するのではなく、

  • code="C" なら CreateCommand クラス
  • code="D" なら DeleteCommand クラス

に非整列化できれば便利だ。

なお、以下の方法で使用するJAXBの実装には、JREに付属のJAXB RIではなくEclipseLink MOXyを使う必要があるので注意*1

方法1: ClassExtractor を使う

以下のようなJavaクラスを作成していく。

commandList/command に対応するクラス

commandList 要素に対応する CommandList クラス。

package example.jaxb.inherit.model;

import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "commandList")
public class CommandList {

    @XmlElement(required = true)
    protected List<Command> command;

    public List<Command> getCommand() {
        if (command == null) {
            command = new ArrayList<Command>();
        }
        return this.command;
    }
}

command 要素の基底クラスである Command クラス。StackOverflow の回答にあるように XmlCustomizer を設定している。CommandCustomizer クラスについては後述。

package example.jaxb.inherit.model;

import javax.xml.bind.annotation.XmlAttribute;
import org.eclipse.persistence.oxm.annotations.XmlCustomizer;
import example.jaxb.inherit.CommandCustomizer;

@XmlCustomizer(CommandCustomizer.class)
public abstract class Command {
// 中身は空
}
command の具象クラス

code="C" の command 要素に対応する CreateCommand クラス。

package example.jaxb.inherit.model;

import javax.xml.bind.annotation.XmlAttribute;

public class CreateCommand extends Command
{
    @XmlAttribute
    protected String data;

    public String getData() {
        return data;
    }

    public void setData(String value) {
        this.data = value;
    }

    public String toString() {
    	return "Create: data=[" + data + "]";
    }
}

code="D" の command 要素に対応する DeleteCommand クラス。

package example.jaxb.inherit.model;

import javax.xml.bind.annotation.XmlAttribute;

public class DeleteCommand extends Command
{
    @XmlAttribute
    protected Integer id;

    public Integer getId() {
        return id;
    }

    public void setId(Integer value) {
        this.id = value;
    }

    public String toString() {
    	return "Delete: id=[" + id + "]";
    }
}
CommandCustomizer

ここがStackOverflowの回答と異なる点。DescriptorCustomizer から属性→クラスのマッピングを設定する方法にはいくつかあるようだが、とりあえず EclipseLink 2.2.0 の時点では以下のように ClassExtractor なるものを使えばうまくいく模様。

package example.jaxb.inherit;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.persistence.config.DescriptorCustomizer;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.ClassExtractor;
import org.eclipse.persistence.oxm.XMLField;
import org.eclipse.persistence.sessions.Record;
import org.eclipse.persistence.sessions.Session;

import example.jaxb.inherit.model.CreateCommand;
import example.jaxb.inherit.model.DeleteCommand;

public class CommandCustomizer implements DescriptorCustomizer {

    @Override
    public void customize(ClassDescriptor descriptor) throws Exception {
        final Map<String, Class<?>> classMapping = new HashMap<String, Class<?>>();
        classMapping.put("C", CreateCommand.class);
        classMapping.put("D", DeleteCommand.class);

        descriptor.getInheritancePolicy().setClassExtractor(new ClassExtractor() {
            // 対象XML要素を表す Record から、非整列化に使用するクラスを返すメソッドを実装
            @Override
            public Class<?> extractClassFromRow(Record databaseRow, Session session) {
                Object indicator = databaseRow.get(new XMLField("@code"));
                return classMapping.get(indicator);
            }
        });
    }
}
メインクラスその他

Eclipse MOXy を使う必要があるので、上記のJavaクラス群と同じパッケージに jaxb.properties というファイルを作成し、以下の内容を記述する。

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory

メインクラスは以下のような感じ。特に何の変哲も無い。

package example.jaxb.inherit;

import java.io.InputStream;
import java.net.URL;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

import example.jaxb.inherit.model.Command;
import example.jaxb.inherit.model.CommandList;

public class Main
{
    public static void main(String[] args) throws Exception
    {
        JAXBContext jaxbContext = JAXBContext.newInstance(
            CommandList.class, CreateCommand.class, DeleteCommand.class);

        URL xmlUrl = Main.class.getClassLoader().getResource("test.xml");
        Source xmlSource = new StreamSource(xmlUrl.toURI().toString());
        
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        CommandList commands = (CommandList)unmarshaller.unmarshal(xmlSource);

        for (Command command : commands.getCommand()) {
        	System.out.println(command);
        }
    }
}
実行

上記の各クラス(+jaxb.properties)とMOXyに必要なライブラリ、test.xml(最初のXML)をクラスパスに配置し、実行すると

Create: data=[新規作成データ]
Delete: id=[1]

のように表示され、codeの値によって正しいクラスに非整列化されていることが確認できる。

方法2: discriminator を使う

この方法は、方法1よりも簡単で、非整列化だけでなく整列化も可能なので、こちらの方がおすすめ。

commandList/command に対応するクラス

ほぼ方法1の定義と同じだが、Command クラスには XmlCustomizer ではなく、XmlDiscriminatorNode を設定する。

import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorNode;

(中略)

@XmlDiscriminatorNode("@code")
public abstract class Command {
// 中身は空
}

これはORMで継承を「単一テーブル継承 (single table inheritance)」や「クラステーブル継承 (class table inheritance)」で扱う場合に、レコードの具象クラスがどれなのかを示すためのカラム*2XML版になっている。

command の具象クラス

code="C" の command 要素に対応する CreateCommand クラス。親クラス Command に指定した XmlDiscriminatorNode の値が "C" であればこのクラスを使う、ということを XmlDiscriminatorValue で設定する。

import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;

(中略)

@XmlDiscriminatorValue("C")
public class CreateCommand extends Command
{
...
}

DeleteCommand クラスにも同様に @XmlDiscriminatorValue("D") を指定しておく。

メインクラスその他

この方法を使った場合、CommandCustomizer クラスは不要になる。

その他のメインクラス・実行手順は方法1と全く同じ。

この方法では、以下のように整列化も可能。

// 前述のMainクラスの続き
Marshaller marshaller = jaxbContext.createMarshaller();
StringWriter writer = new StringWriter();
marshaller.marshal(commands, writer);
System.out.println(writer.toString());

出力結果:

<?xml version="1.0" encoding="UTF-8"?>
<commandList><command data="hoge" code="C"/><command id="1" code="D"/></commandList>

おまけ: MOXyのアノテーションを付けられない/付けたくない場合

Command/CreateCommand/DeleteCommand クラスに MOXy 固有のアノテーションを付けたくない場合は、以下のようにする。

OXM設定ファイルの作成

以下のような MOXy の OXM(Object-XML-Mapping) 設定ファイルを作成する(これは方法2の場合)。

<?xml version="1.0"?>
<xml-bindings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.eclipse.org/eclipselink/xsds/persistence/oxm http://www.eclipse.org/eclipselink/xsds/eclipselink_oxm_2_3.xsd"
  version="2.3">

  <java-types>
    <java-type name="example.jaxb.inherit.model.Command" xml-discriminator-node="@code" />
    <java-type name="example.jaxb.inherit.model.CreateCommand" xml-discriminator-value="C" />
    <java-type name="example.jaxb.inherit.model.DeleteCommand" xml-discriminator-value="D" />
  </java-types>

</xml-bindings>

上記の内容を command-metadata.xml ファイルとしてクラスパス上に配置適当しておく。

Command/CreateCommand/DeleteCommand の修正

各クラスから、MOXy関連のアノテーション

  • @XmlDiscriminatorNode
  • @XmlDiscriminatorValue

を削除する。

@XmlAttribute 等はそのままでよい。

メインクラスの修正

メインクラスの最初で、以下のように設定を行う。

import org.eclipse.persistence.jaxb.JAXBContextFactory;

(略)

    public static void main(String[] args) throws Exception
    {
        Map<String, Source> metadata = new HashMap<String, Source>();
        // クラスパス上に配置した場合。
        // カレントディレクトリに配置した場合は単に new StreamSource("command-metadata.xml") でよい
        URL metadataUrl = Main.class.getClassLoader().getResource("command-metadata.xml");
        metadata.put(CommandList.class.getPackage().getName(), new StreamSource(metadataUrl.toURI().toString()));
        
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY, metadata);
        
        JAXBContext jaxbContext = JAXBContext.newInstance(
                new Class[]{CommandList.class},
                properties);

        // あとは同じ

この辺の詳細については EclipseLink/Examples/MOXy/EclipseLink-OXM.XML - Eclipsepedia を参照。

*1:Maven2なら EclipseLink/Maven - Eclipsepedia を参照

*2:ActiveRecordならtypeカラム、JPAなら@DiscriminatorColumnとか

AppDomainをスレッドごとに分ける

PC上で使うことだけを想定されて作成された、static変数にデータを入れるようなライブラリをWebサービスで使わなければならない。簡単に言うとこんなライブラリ。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyLib
{
    public class MyClass
    {
        private static string _message;

        public string Message
        {
            get { return _message; }
            set { _message = value; }
        }

        public string greeting(string name)
        {
            return _message + ", " + name;
        }
    }
}

これをマルチスレッド環境で正しく動作させるため、スレッドごとにAppDomainを分けてみる。

using System;
using System.ComponentModel;
using System.Reflection;
using System.Threading;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;

using MyLib;

namespace MyService
{
    // Webサービスメソッド
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ToolboxItem(false)]
    public class MyService : WebService
    {
        [WebMethod]
        public string MyMethod(string message, string name)
        {
            // このインスタンスとそのクラスは、全てスレッドローカル
            // static変数もスレッドごとに異なる
            MyClass instance = ThreadLocalMyClass.Instance;

            // class を使った処理…
            clazz.Message = message; 
            return instance.greeting(name);
        }
    }

    internal class ThreadLocalMyClass
    {
        [ThreadStatic]
        private static AppDomain _domain;

        [ThreadStatic]
        private static MyClass _instance;

        private static AppDomain Domain
        {
            get
            {
                if (_domain == null)
                {
                    _domain = AppDomain.CreateDomain(
                        String.Format("AppDomain{0}", Thread.CurrentThread.GetHashCode()),
                        null,
                        AppDomain.CurrentDomain.SetupInformation);
                }
                return _domain;
            }
        }

        public static MyClass Instance
        {
            get
            {
                if (_instance == null)
                {
                    // 引数はnamespace、クラス完全修飾名
                    _instance = (MyClass)Domain.CreateInstanceAndUnwrap(
                        "MyLib", "MyLib.MyClass");
                }
                return _instance;
            }
        }
    }
}

こんな感じで良いのかねぇ。

ThreadStaticって楽ですね。

Windows上のRubyでUnicodeファイル名をglobできるようになっていた

Ruby 1.9.2 から、WindowsのDir.globでSJIS範囲外のファイル名も取得できるようになったらしい。

森鷗外.txt」ファイルがあるディレクトリでの例↓。パターン文字列をUnicodeエンコーディングにすれば良いらしい。

> irb

Dir.glob("*.txt".encode('utf-8'))
# => ['\u68EE\u9DD7\u5916.txt']

Dir.glob("*.txt".encode('utf-8')).map {|f| f.encoding }
# => [#<Encoding:UTF-8>]

Dir.glob("*.txt".encode('utf-8')) {|f| File.delete f }
# => (問題なく削除される)

エンコーディングを変えないと今まで通りWindows-31Jのままなので、範囲外の文字が ? になってしまう。

> irb

Dir.glob("*.txt")
# => ['森?外.txt']

Dir.glob("*.txt").map {|f| f.encoding }
# => [#<Encoding:Windows-31J>]

Dir.glob("*.txt") {|f| File.delete f }
# => Errno::EINVAL (invalid argument)

マニュアルには載ってないけど、パターン文字列と同じエンコーディングでファイル名を取得するようになった、ということかな?