おがさわらなるひこのオープンソースとかプログラミングとか印刷技術とか

おがさわらなるひこ @naru0ga が技術系で興味を持ったりなんだりしたことをたまーに書くブログです。最近はてなダイアリー放置しすぎて記事書くたびにはてな記法忘れるのではてなブログに移行しました。

クリエイティブ・コモンズ・ライセンス
特に断りがない場合は、本ブログの筆者によるコンテンツは クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。

jOpenDocumentを2021年にリブートしてみる その4:GitHub ActionsでJDKを振って自動テスト

前回の記事はこちら。

naruoga.hatenablog.com

やはり2021年にもなって、手元でテスト回したりするのは犯罪でしょう。普通にpushしたりPull-Req作ったりしたらビルドのチェックとテストぐらいはしたい。

ということで、恥ずかしながら今まで使ったことがないGitHub Actionsを使ってみることにしました。

とりあえずActions登録

今更なのですがQuick Startを読みます。

docs.github.com

で、実は今やってる作業は締め切りがある話で、めちゃくちゃ締め切り迫ってるので、真面目に調べてる暇ない!って感じで、 こういう付け焼刃ホントよくないんですけど、このリポジトリにサンプルがいっぱいあるよってことで即見に行きました。

github.com

はいはい、顧客が本当に必要になったものありました。Mavenでビルドするための情報。

github.com

さくさくパクりまして、 .github/workflows/mvn-build-actions.yml というファイルをこんな風に書きました*1

name: Java CI with Maven

on:
  push:
    branches: [ $default-branch ]
  pull_request:
    branches: [ $default-branch ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up JDK 11
      uses: actions/setup-java@v2
      with:
        java-version: '11'
        distribution: 'adopt'
    - name: Build with Maven
      run: mvn -B package --file pom.xml

で、こいつを master ブランチに入れて、push時のトリガを強制的にかけるために

touch foobar
git add foobar
git commit -m "GitHub Actions test"
git push

とかやったんですが動かない……なんぞ? なんかスキーマの書き間違い? でもサンプルのコピーだしなあ。

Actionsがトリガされない問題のチェック

Actionsの画面を見ると、手動実行のUIっぽい奴も特に存在しない……ううむ。

これは調べてみたら、

docs.github.com

ちゃんと答えが書いてあって、 workflow_dispatch ってトリガを追加すればいいらしいですね。で、こんな感じにした:

on:
  workflow_dispatch:
  push:
    branches: [ $default-branch ]
  pull_request:
    branches: [ $default-branch ]

ら、手動実行のボタンが出てきました。よしよし。ということはスキーマが全然違ってて認識できてないみたいなことはないらしい。

やれやれということで手動で実行してみたら、一回目は

Error:  Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project jOpenDocunentNg: Execution default-test of goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test failed: Plugin org.apache.maven.plugins:maven-surefire-plugin:2.12.4 or one of its dependencies could not be resolved: Could not transfer artifact org.apache.maven.shared:maven-common-artifact-filters:jar:1.3 from/to central (https://repo.maven.apache.org/maven2): transfer failed for https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-common-artifact-filters/1.3/maven-common-artifact-filters-1.3.jar: Connection reset -> [Help 1]

とかなんとか言われて、えっ、手元ではビルドできてるんだけどな……なんでや……ってなったのですが、 ダメモトでもっかい再実行したらうまく行って、あとはずっと成功してます。 なんじゃそりゃ……と思いますが、まあ、いいでしょう。

それとは別にいくつかのサンプル見たら、そもそも push とか pull_requestbranch 指定してないのが普通っぽいので、それを取り除いて

on:
  workflow_dispatch:
  push:
  pull_request:

こうしたら、普通に push した場合でもActionsがキックされるようになりました。めでたしめでたし。

複数のJDKでビルドする

こういうときってジョブを割るのがいいのかActionを割るのがいいのか、たぶんちゃんとどこかにどうすればいいってプラクティスがあるんでしょうけど、 前述のように時間がない中付け焼刃でやってるので、雑にActionを割りました。 こういうときも共通部分をどこかに記述して差分(今回の場合はJDKのバージョン)だけを記述する方法が絶対あると思うのですが、 それはあとで考えるとして、いったんはJDK11側のファイルはこんな感じにしました。

name: Build with JDK11

on:
  workflow_dispatch:
  push:
  pull_request:

jobs:
  build-on-jdk11:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Set up JDK 11
        uses: actions/setup-java@v2
        with:
          distribution: 'adopt'
          java-version: '11'
      - name: Build with Maven
        run: mvn -B package --file pom.xml

JDK8のファイルは11って書いてあるところが8ってなってるだけです……お恥ずかしい。

まあでも、いちおうこうやって二つ並んで、テストがそれぞれ走るようになりました。よかったよかった。

f:id:naruoga:20210716182138p:plain
JDK8とJDK11の両方でCIできるようになった

テストレポートがGitHubの画面から見たい

ここまででまあ一応「pushやPRのたびに自動テストをする」目的は達したんですが、Jenkins + JUnit プラグインを使ってた人間としてはやっぱり非常に寂しい。 ダッシュボードの上からテスト結果が見たいなあと思って、雑にググったらこういうプラグインがヒットしました。

github.com

で、各yamlファイルにこんな感じで書いてみました。

    steps:
      - uses: actions/checkout@v2
      - name: Set up JDK 11
        uses: actions/setup-java@v2
        with:
          distribution: 'adopt'
          java-version: '11'
      - name: Build with Maven
        run: mvn -B package --file pom.xml
      - name: Publish Test Report
        if: ${{ always() }}
        uses: scacap/action-surefire-report@v1
        with:
          check_name: JDK11 result

結果は……ちょっと期待と違って、

f:id:naruoga:20210716182649p:plain
片方のActionに両方の結果が出ちゃう

こんなふうに、片方のActionに両方の結果が出ちゃってます。中身もこんな感じで、ちょっと寂しい。

f:id:naruoga:20210716182822p:plain
履歴とかはなくてその時の結果の数しか見られない

Failしたときの結果も見てみたいけど、このプラグインの設定をもっと詰めるか、ほかのプラグインを探すかも含め、後日ですねー。

READMEにバッジをはる

つまらないことですがCIの結果をREADMEにバッジとして貼ると、なんか今風じゃないですか。ので貼ることにしました。

それぞれのActionの画面の右上の「…」ボタンを押して、「Create status badge」を押すと、

f:id:naruoga:20210716183133p:plain
メニューからバッジ作成

こんなダイアログが出てきて、README貼り付け用Markdownスニペットが得られます。

f:id:naruoga:20210716183252p:plain
バッジのMarkdown記法が得られる

これをREADMEにぺったんすると、

f:id:naruoga:20210716184021p:plain
バッジが並んで今風(そうか?

これでなんかモダンな開発をしているような雰囲気を醸し出すことができた……かな?

まあそんなわけで、まだまだやり残したことはありますが(というかまだ目的全然果たせてないですが)今回の短期集中連載はここまで。

今後の宿題としては:

  • XMLのValidatorをちゃんと動かす(最優先)
  • マトモなサンプルでちゃんと動くかを確認する(具体的には私の所属先のツール)
  • テストをきれいに直す
  • 会社名義でMaven Centralにpublishする
  • ODF 1.3対応をする
  • 下回り(ODF操作部分)を独自実装ではなくODF Toolkitに乗り換え

あたりかなー。協力者も募集ですよ。

*1:今回、JAR配布の予定はないので -B test でも十分なんですけど、まあ、もしかしたらするかもしれないし。

jOpenDocumentを2021年にリブートしてみる その3:落ちてるテストを成功させる

naruoga.hatenablog.com

のつづき。おつむの出来がしょぼいのでXMLスキーマValidationがコケてる理由がわからないため、ほかのテストのFailを潰していきます。

幸いなことにValidatorはテスト内部でしか使っていないので、これでもアプリ側から使う分には問題ない……はず*1

落ちてるテストを拾う

でも、実はValidation以外で落ちてるテストって二つだけなんですよねえ……。

f:id:naruoga:20210716112037p:plain
落ちてるテストは日付と時刻関係だけ

しかも落ち方が……あやしい。これはテストのGlobalization対応が不十分なんじゃないか?

f:id:naruoga:20210716112213p:plain
落ち方がいかにも怪しい

DataStyleTest.testFormat() を直す

まずは簡単そうなほうから。

junit.framework.ComparisonFailure: 
Expected :0,500
Actual   :0.500

これは絶対、Locale関係だろ……と思うので、見てみましょう。

落ちてるところまでコードを引用するとこんな感じ。

    public void testFormat() throws Exception {
        final ODPackage pkg = new ODPackage(this.getClass().getResourceAsStream("/cellFormat.ods"));
        final Sheet sheet = pkg.getSpreadSheet().getSheet(0);

        // * test that the framework format as OpenOffice
        final int lastRow = sheet.getCurrentRegion(0, 0).getEndPoint().y;
        for (int i = 0; i <= lastRow; i++) {
            final MutableCell<SpreadSheet> cell = sheet.getCellAt(0, i);
            final String byOO = cell.getTextValue();
            final ODValueType origType = cell.getValueType();
            final Object cellValue = cell.getValue();
            // like OO, we should allow any value without removing the data style
            cell.setValue("string");
            cell.setValue(12.3);
            cell.setValue(new Date());
            cell.setValue(true);
            cell.clearValue();
            if (origType != null)
                cell.setValue(cellValue, origType, false, false);
            assertEquals(byOO, cell.getTextValue()); // <---- ここで落ちてる
            assertEquals(origType, cell.getValueType());
        }

こら! テストコードで for 文書くなって教わらなかったのか! と怒りたいところですがそれはおいといて。

まずは読み込んでるファイルは cellFormat.ods というCalcファイルですね。 中身はこんな感じ。

f:id:naruoga:20210716130933p:plain
cellFormat.odsの内容

で、この1列目を各行ぐるぐる回ってセルの各要素のテキスト文字列を取り出した後、セルに書式指定を無視していろいろ値をセットして(表計算ソフトでは例えば数値書式のセルに文字列を突っ込んだりもできるので)、元の値と書式をセットしなおして結果が変わらないかを見る……って、いうのがテストの意図っぽいですね。

そして、expected が 0,500 で actual が 0.500 なので、元のドキュメントの標準言語はまあ、想像したとおりフランス語なので小数点は , で、だから取り出したセルの値は 0,500 で、cell.setValue() したときには私の動いてる環境の標準ロケールだから日本語で小数点が変わっちゃう……って、ことじゃないかな?

ん? じゃあ、これ、テストの問題じゃなくてプロダクトコードの問題じゃないですかね……。

ともかく、問題になってるのは2行目(セルの表記的には (0,1))なので、そこに条件付きブレークポイントを貼って調べますと、……あれ?

f:id:naruoga:20210716134246p:plain
contentがTextで0,500ってなってる!

テキストじゃん!

あーでも、attributes はこうなのか……。

f:id:naruoga:20210716134601p:plain
attributeでは確かにtype:floatになってる

ふむふむ、ODFの仕様をちゃんと読んでないことがバレバレなのですが、どうやらODFでは

  • セルに文字列で表示するための要素( cell.localElement.content[0]
  • セルに実際に入っている値( cell.attributes

を二重管理してて、前者は少なくともLibreOffice上では使われてない(セルの値と書式から動的に表示結果を作ってる)ってことなのでしょうか……。

で、

cell.setValue(cellValue, origType, false, false);

すると、「ドキュメント」のではなく「セル」の言語が使われて、0.500 になっちゃう、ってこと……みたいです。

……いや、違うかな。違いそう。何が違うかというと

「ドキュメント」のではなく「セル」の言語が使われて

です。というのはなぜかというと、cell.setValue() の実装をおいかけてくと、最終的に NumberStyle.format() ってメソッドに来るんだけど、

    @Override
    public String format(Object o, CellStyle defaultStyle, boolean lenient) {
        final Number n = (Number) o;
        final Namespace numberNS = this.getElement().getNamespace();
        final StringBuilder sb = new StringBuilder();
        @SuppressWarnings("unchecked")
        final List<Element> children = this.getElement().getChildren();
        for (final Element elem : children) {
            if (elem.getNamespace().equals(numberNS)) {
                if (elem.getName().equals("text")) {
                    sb.append(elem.getText());
                } else if (elem.getName().equals("number") || elem.getName().equals("scientific-number")) {
                    sb.append(formatNumberOrScientificNumber(elem, n, defaultStyle));
                } else if (elem.getName().equals("fraction")) {
                    // TODO fractions
                    reportError("Fractions not supported", lenient);
                    sb.append(MutableCell.formatNumber(n, defaultStyle));
                }
            }
        }
        return sb.toString();
    }
}

この中で呼ばれている formatNumberOrScientificNumber(elem, n, defaultStyle)defaultStyle に保持されてるセルのロケール情報見てないっぽい……ですね。 なのでこれはプラダクトコードの問題。

だけど、LibreOfficeというアプリケーションで使うことだけを考えると、テキストは付帯情報であって実際の値やフォーマット情報は適切に扱われているので、ここに不具合があってもすごくシリアスではないし、まあいいかなあという……FIXMEフラグだけつけときますか。

こういうこと考えると下周り(ODFを実際に操作する部分)をごっそりモダンなライブラリ……例えば:

github.com

に差し替えたくなりますね。

ということで乱暴ながら、

  • testFormat() の今落ちてる assert はコメントアウト
  • それ以外の assert は、期待値が即値なのが問題なのでいったん String.format() を利用するように書き換え(将来はセルのロケールに従った動きをしないとだめですが)

としました。

DataStyleTest.testDays() をどうするか

先ほども書いたけど、このテストでは期待値と実際の値が1時間ずれてるというもの。

Expected :27/10/2013 09:30:00
Actual   :27/10/2013 10:30:00

1時間ってことはタイムゾーンというか夏時間(DST)がおかしいんじゃない?と思うんですが、なんかコード見るとまさに夏時間向けの処理がごちゃごちゃと書いてある……。 これを直さなきゃいけないのかー。

と、思いましたが、 このテスト、ちらっと見た感じだと jOpenDocument 自体のテストをしてるように読めないんですよねー。

なので、えいっと無視することにしました。

mavenでdependsしてるのは JUnit4 なので、 @Ignore って書くだけじゃん、と思ってそう書いたら 「これはJUnit3形式のテストだから @Ignore じゃ無視されなくて、メソッド名を test で始まらないように _test ってすればいいよ」とIntellij IDEA君に教えてもらいました。 なんと、まあ。

JUni4 形式に移行することもIntellij IDEAの機能でできるらしいのですが(優秀)、 個人的には将来はJUnit5でもちょっとましなテストを書こうと思ってるんで*2 、 雑に _ をつけて無視する方法にしました。

動かないXMLのバリデーション

ここまで来たら毒皿なので、ここらへんも無視することにしちゃいましょう。

主にバリデーションがテストしたい部分で、これを通さないと落ちるテスト

  • OOXMLTest.testValidation()
  • SheetTest.testCreate()

についてはやはりIgnore(FIXMEつけて)。

それ以外のテストで、テストメソッド内にて SheetTest.assertValid() を呼んでいるテストについては、システムプロパティ jopendocument.test.validationtrue を指定しない限り無視するようにしました。 これも後々は直すつもりですが、まずは先に進みたいので。

ま、そんなわけで、テストは全部バスするようになりました*3

次はCI的なところをやっていきます。

*1:もちろん、スキーマをぶっ壊す可能性があるようなODF操作ライブラリとか怖くて使いたくないので、いずれは直しますよ、もちろん。ただ、優先度をいったん置くってことです。

*2:for文書くな、一つのテストメソッドで延々違うassertを繰り返すんじゃない、的な意味で。

*3:これは品質を満たしているという意味ではないので、まったくよろしくないけど。

jOpenDocumentを2021年にリブートしてみる その2: テストを動かす

前回の記事

naruoga.hatenablog.com

のつづき。今回は目標が低くてテストを動かすところまで。 これが全然苦戦して、ほとんど進捗がないんですねえこれが。

テストの位置をMaven標準に動かす

その前に前の記事の落穂ひろいですが、

To compile you need to put iText ( http://www.lowagie.com/iText/download.html ) and junit4 ( http://www.junit.org/ ) into the lib/ dir.

って書いてあるので、iText と Junit4 が必要ですね。ということで pom.xml に次の二つを追加しておいてました。

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>5.5.13.2</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
</dependency>

(なんで最初からlibディレクトリに入ってないんでしょ、再配布制限的な何かあるのかな)

それとこれも前回からの書き漏れですが、 src/product.properties を参照できなくてコケるので src/main/resources に移動しておきました。 ここまでは前回の落穂ひろい。

で、ここからが今回の本題ですが、junit4が必要だよと言ってるってことは当然テストコードがあるわけで、 Maven的には src/test/java 以下にソースがないとたぶんだめですよね。

まあこれはきっと @Test アノテーションがあるファイルはテストコードだと見極めをつけて移動しました。

$ grep -rl "@Test"
src/main/java/org/jopendocument/dom/ODSingleXMLDocumentTest.java
src/main/java/org/jopendocument/dom/OOXMLTest.java
src/main/java/org/jopendocument/util/cache/ICacheTest.java

こいつらをまるっと test に(同名のフォルダを掘って)移動。 すると Intellij IDEA上でテストコードが実行できるようになりました。やった!

で、まあテストはrunできるようになったんですが結果を見ると真っ赤っ赤。 あとはぼちぼちこれを直していけばよかろうと。

リソースの移動

まずはテストコードの中で指定されてる .odt がないとテストコード内でファイルが読めないので移動します。これは簡単。 diff見るとこんな感じですね。

diff --git a/src/main/java/org/jopendocument/dom/empty.odt b/src/test/resources/org/jopendocument/dom/empty.odt
similarity index 100%
rename from src/main/java/org/jopendocument/dom/empty.odt
rename to src/test/resources/org/jopendocument/dom/empty.odt
diff --git a/src/main/java/org/jopendocument/dom/styles.odt b/src/test/resources/org/jopendocument/dom/styles.odt
similarity index 100%
rename from src/main/java/org/jopendocument/dom/styles.odt
rename to src/test/resources/org/jopendocument/dom/styles.odt
diff --git a/src/main/java/org/jopendocument/dom/test.odt b/src/test/resources/org/jopendocument/dom/test.odt
similarity index 100%
rename from src/main/java/org/jopendocument/dom/test.odt
rename to src/test/resources/org/jopendocument/dom/test.odt

で、よく見ると、普通に src/main/java にもいかにもリソースなファイルがあるので移動しとかないとですね。 具体的には src/main/java/org/jopendocument/dom/oofficeDTDs/ にあるやつを丸っと src/main/resources/ 以下に移動します。

ほかにも getResource(AsStream)() で参照されてるファイルを探してはリソースに放り込む日々。

XML関係のテストがそもそも動いてない

これで結構テスト通るようになってきたんですが、問題は、XML関係のテストでスキーマによるバリデーションを試みてるところで落ちてる。 具体的には

private Schema createSchema(final String name) throws SAXException {
    return SchemaFactory.newInstance(XMLConstants.RELAXNG_NS_URI).newSchema(getClass().getResource("/oofficeDTDs/" + name));
}

こんなコードが、

Caused by: java.util.ServiceConfigurationError: javax.xml.validation.SchemaFactory: org.iso_relax.verifier.jaxp.validation.RELAXNGSchemaFactoryImpl Unable to get public no-arg constructor

こういう例外を吐いて落ちるんですな。

なぬ?スキーマに対する適切なコンストラクタがない? いやいや、XMLConstants.RELAXNG_NS_URIJava標準なんだからそんなわけないだろう……と、Java力(ちから)がないわたくしは考えておりました。

しかし色々調べると「java.xml.validation のデフォルトの実装に含むべきなのはW3Cなソレだけで、それ以外のスキーマを利用したい場合は自分で何とかしなさい」ってことらしいじゃーないですか。おうふ。なるほど……。ここでしばらく停滞、袋小路にハマってました*1

でもよく見るとREADMEにこんな記述がありました。

To validate XML (needed for JUnit tests) you need to download http://java.net/downloads/msv/releases/msv.20090415.zip and put msv.jar, relaxngDatatype.jar, xsdlib.jar and isorelax.jar in the classpath.

いや、だったらなんで最初から入れといてくれないの……というのはともかく、これらをMavenの依存関係に足せばよいのですね。足しましょう。足しました。

<dependency>
    <groupId>msv</groupId>
    <artifactId>msv</artifactId>
    <version>20050913</version>
</dependency>
<dependency>
    <groupId>msv</groupId>
    <artifactId>relaxngDatatype</artifactId>
    <version>20050913</version>
</dependency>
<dependency>
    <groupId>msv</groupId>
    <artifactId>xsdlib</artifactId>
    <version>20050913</version>
</dependency>
<dependency>
    <groupId>msv</groupId>
    <artifactId>isorelax</artifactId>
    <version>20050913</version>
</dependency>

これで行けるだろ!と思ったんですが、いざValidationをしようとするとコケるのです……しくしく。

@Override
public Validator getValidator(final Document doc, final boolean ignoreForeign) {
    final Schema schema;
    try {
        if (doc.getRootElement().getQualifiedName().equals("manifest:manifest"))
            schema = this.getManifestSchema();
        else
            schema = this.getSchema();
    } catch (SAXException e) {
        throw new IllegalStateException("relaxNG schemas pb", e);
    }
    return schema == null ? null : new Validator.JAXPValidator(doc, ignoreForeign ? UNKNOWN_PRED : null, schema);
}

こういうコードで、 this.getSchema() したところで

Caused by: org.xml.sax.SAXParseException; systemId: file:/C:/Users/naruhiko/gosrc/src/github.com/naruoga/jOpenDocument/target/classes/oofficeDTDs/OpenDocument-schema-v1.1.rng; lineNumber: 123; columnNumber: 36; 同名のパターンが既に定義されています。combine属性を使って結合するか、パターン名を"office-meta-content"以外のものに変更してください

って例外が出ちゃうのです(しかし上のコードの "relaxNG schemas pb" ってどういう意味なんだろう。pbってスイスの工具屋さんじゃないよね)。

うーん。わからん。言葉通りだとすると元のスキーマに問題があるっぽく読めるけどスキーマは何にも変えてないしなあ(当たり前)。

office-meta-contentgrepしてみると

 grep -r "\"office-meta-content\""
src/main/resources/oofficeDTDs/OpenDocument-schema-v1.1.rng:            <ref name="office-meta-content"/>
src/main/resources/oofficeDTDs/OpenDocument-schema-v1.1.rng:<define name="office-meta-content">
src/main/resources/oofficeDTDs/OpenDocument-strict-schema-v1.1.rng:        <define name="office-meta-content">

となってるのは確かにそうなんだけど、これがいいのか悪いのか……何も変えてないので悪いってことはないと思うし、そもそもスキーマなんだから標準から引っ張ってきてると思うんで、なにがなんやらさっぱりです。

わからないので後回しにして*2落ちてるほかのテストを調べることにします。次回へ続く。

*1:関係ないけど、そして昔からJavaやってる人なら当たり前すぎることなんですが、RelaxNGについて調べたらJenkinsの創始者で現Launchableの川口さんが出てきて、あー世界はこんな風につながっておるのだなと思いました(?)。

*2:こうやって書いておくと優しい人が誰かSNSで教えてくれたりしないかなとかすかに期待しつつ……。

jOpenDocumentを2021年にリブートしてみる その1:Maven化

LibreOfficeの標準フォーマットであるOpenDocument Format(以下ODF)*1 は、XMLの標準化団体OASISのOpenDocument TCが標準化しISO標準にもなっている公開された文書フォーマットです。アプリケーションの進化に伴い標準が改訂されていく「真の国際標準」である唯一のオフィス向けドキュメント形式であるとともに、スキーマの構造が透明で見通しがよく、なおかつLibreOfficeの出力するODFは意味が明瞭(機械も人間も読みやすい)という特徴があります。

そのため操作・加工ライブラリも豊富にあり、さまざまな言語でODFを簡単に、しかも結構複雑な操作を行うことができます。

さて、私の勤務先はとあるセキュリティベンダーでございまして、日々顧客のためにシステムやアプリを診断して、それを報告書にして提出するということやっております。で、スプレッドシートの形式になった診断結果から見やすいPDF形式の報告書を作成するために、社内製のツールにてODFファイル(Writer向けのODT)を生成して、それに考察などを記入してWriterでPDFエクスポートして提出するという作業を日々行っております。

このために使っているライブラリが

www.jopendocument.org

といいまして、専用の拡張機能で埋め込んだタグをプログラムで置換したり表形式で埋め込んだりするというなかなか使いやすいライブラリであります。こいつ便利ですよという発表は去年の台湾のOSSイベントCOSCUPでしました。

speakerdeck.com

で、この資料でも書いたんですが、jOpenDocumentいいんですけどなにせ最後に出たバージョン1.4-rc2が 2014年 でして、java8で動作させると「この言語機能はdeprecatedだから使うのやめた方がいいよ」という警告がバリバリ出る。それはまあいいのですが、ODF 1.2 extended までしかサポートしてないので、今のLibreOffice(7.0以降)で普通に生成したODF(1.3 extended)だと正しく扱えない。 しかし今更これらの問題が治るとはとても思えない。思えないんだったら、自分で直せばいいじゃん! と、トライしてみることにしました。

ちなみにわたくし齢50にして細々と今でもコードを書いてますが(会社で書くのは ↑ のスライドでも述べた通りScala)、職業プログラマーとしての黄金期?はC/C++(言語仕様的には98)、C++は組み込み用途だったのでメモリフットプリントを抑えるためにRTTI禁止、という制限があり、Javaを書くようになったのは前職のソフトウェア第三者検証会社(今の所属の親会社)に入ってからで、世の中にはGradleとIntellij IDEAがすでに存在し、クラスパスがどーとか javac でビルドして *.class から JAR 作るとかそういう経験はゼロなのであります。ゼロ。antも知らない。mavenもほぼわからない。

そんなレベルの人間が、2014年で開発が止まっちゃった、まったくの他人が書いたライブラリをモダンにしてみようという、まあうまく行くんだか行かないんだかという試みを紹介してみたく。 意味も分からず試行錯誤で進めてるところがあるので、「そんなやり方はよくないよ!」という突っ込みは大募集なのです。

もとのソースを取ってくる

ソースは公式のダウンロードページ にてZIPファイルでおいてあるので、まずはこれを取ってきて手元に展開します。

まずは最初にライセンスを確認。LICENSE.txt を見たら GPL v3 だったので大手を振ってフォークできます。オープンソース万歳。

で、中身を見てみたら、 build.xml というファイルと libs というフォルダに JAR ファイルがゴロゴロ転がっています。さっき言った通り私はJavaのビルドシステムはさっぱりわからんのですがきっとこれはantなんだろうなと。で、さすがに今リブートのであればせめてビルドシステムはモダンにしたいよねということで、Maven化することにしました。

これも繰り返しになりますが、わたくし別にMavenにも詳しいわけではありません。なので、何も考えずにIntellij IDEAの新規プロジェクトでMavenを選んで、こんな感じの pom.xml をスクラッチで作りました。

<?xml version="1.0" encoding="UTF-8"?> 4.0.0

<groupId>jp.shiftsecurity.tech</groupId>
<artifactId>jOpenDocunentNg</artifactId>
<version>2.0-SNAPSHOT</version>

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

依存関係の解決

わからないなりに、たぶん元プロジェクトの lib にあるJARをMaven Centralで探してその設定を pom に書いていけばいいんでしょ、ということにしてやってみることにしました。なにせ2014年のソースなので依存関係も2014年で止まっておりますが、そこは何も考えずに最新化して、動かなかったら考えようと……。

探し方としてはまあひたすら https://search.maven.org/ で検索するだけなんですが、

ファイル名でartifactが想像できるものはそれで いまいちわからないものは jar を unzip してパッケージ名から推察 って感じです。結果以下の通りになりました。

元のjar maven central で見つけたバージョン 備考
commons-collections-3.2.1.jar org.apache.commons:commons-collections4:4.4 この名前なら多分Apache Commonsでしょう。3と4で非互換あるかもだけどなんとかしましょう
fb-annotations-2.0.0.jar com.github.spotbugs:spotbugs:4.3.0 もとのはfindbugsらしいけど死んだので後継プロジェクトのspotbugsに
isorelax-jaxp-bridge-ILM.jar org.jopendocument:isorelax-jaxp-bridge-ILM:1.1 どうもjOpenDocumentの一部を切り出しただけで凍結されてるけど、動かなかったら考えることでいったんはそのまま取り込み(実際問題あったんだけど)
jaxen-1.1.6.jar jaxen:jaxen:1.2.0
jcip-annotations.jar net.jcip:jcip-annotations:1.0 いろいろforkがあるけどいったんは公式っぽいもので
jdom-1.1.1.jar org.jdom:jdom:1.1.3 たぶん1系の最新を選んどけばよいのだろうと
jdom-2.0.5.jar org.jdom:jdom2:2.0.6 同名のJARのバージョン違いがあるのがなんだけど、まあ2系の最新を選べばよかろうと
js-1.7R1.jar org.mozilla:rhino:1.7.13 ZIPファイルをほどいたパッケージ名から判断
ognl-2.6.9.jar ognl:ognl:3.2.21 これは素直に名前から。
ognl-engine.jar org.jopendocument:ognl-engine:2.6.9 これも名前から

で、これらのdependencyをpom.xmlに追加してビルド……するも、jaxenがmaven centralに見つからないと怒られてしまいました。 よくわからないし、必要なら依存関係で引っ張ってこられたりするかな……と、えいっと指定自体はずしてしまいました。 いまのところ特に何も起きてない、ような……。

ソースコードの場所を移動

Mavenなのでソースコードの位置を src/ 直下から src/main/java 以下に丸っと引っ越しました。 テストコードも混じってるんですがそれはあとで……。

ライブラリ更新に伴うソースコードの修正

ライブラリのバージョンをばしばし上げたので、当然非互換な部分も出てきます。 動作の部分はあとで考えるとして、とりあえずコンパイルエラーをつぶしていきます。 細かに書いても退屈なだけでしょうから下記リンクからdiffを見てください。

https://github.com/naruoga/jOpenDocument/compare/af77a4b09e7fea72ceff9f325919bc463ddc7414..92982f10d37a9cf230720246fa2d23692cdd8e3f

コミットコメントから引用しておきます。英語へぼいのは勘弁してね。

commit 92982f10d37a9cf230720246fa2d23692cdd8e3f
Author: Naruhiko Ogasawara <ogasawara@shiftsecurity.jp>
Date:   Fri May 7 21:08:00 2021 +0900

    avoid to use deprecated PdfTemplate#createPrinterGraphics

    the implementation is just call PdfPrinterGraphics2D constructor,
    so it could be migrate easily

commit 75caf0495319ab02c60d0e5b7524d85808b31893
Author: Naruhiko Ogasawara <ogasawara@shiftsecurity.jp>
Date:   Fri May 7 21:05:28 2021 +0900

    OGNLDataModel: remove previous code comment, because we use Git

commit d2855abc7f01d43b8b0dd30b33bb1d4b96c655ed
Author: Naruhiko Ogasawara <ogasawara@shiftsecurity.jp>
Date:   Fri May 7 20:48:49 2021 +0900

    follow updated APIs of libraries

    general: avoid useless FQCN

    several: migrate to org.apache.commons.collections4
      - CollectionUtils
      - ExnTransformer
      - ICache
      - IPredicate
      - ITransformerWrapper
      - ODSingleXMLDocument
      - Transformer

    CollectionMap2Itf:
      remove() should have Object args, not type parameters to override Map

    OGNLDataModel:
      Ognl.getMemberAccess() has been removed, then change
      Ognl.addDefaultContext() without it

    SimplePDFGenerator:
      migrate to com.itextpdf.text from com.lowagie.text (this was obsoleted)

pom.xml にビルドの設定とプラグインの設定を追加

これでビルドできるようになっただろ! と IntellijMaven画面から build を叩いても何も起きない。ふむぅ。

これは私が素人だからあんまり意味も分からずに書いてますが、以下のようなビルド用の設定書かないとだめっぽいですね。

        <dependency>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-source-plugin</artifactId>
            <version>3.2.1</version>
            <type>maven-plugin</type>
        </dependency>
    </dependencies>
    <build>
        <resources>
            <resource>
                <directory>${project.basedir}/src/main/resources</directory>
            </resource>
        </resources>
        <testResources>
            <testResource>
                <directory>${project.basedir}/src/test/resources</directory>
            </testResource>
        </testResources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.2.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>3.2.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

maven-resource-pluginはJARの中にリソース取り込むためのプラグインmaven-soruce-pluginはsource jarを作るときのプラグイン(これがないとJARで取り込んだプロジェクト側でソースコードレベルでバッグができない)、あとビルド時にどこからリソース参照するかの設定を追加。この時点でのpom.xmlはこんな感じです。

jOpenDocument/pom.xml at 6497e9f279bb8d6184b8994a452865e040b0bc5d · naruoga/jOpenDocument · GitHub

これで、IntellijMaven 機能でbuildすると依存関係をちゃんと抱いたjarと、source jarの両方ができるようになりました。よかったよかった。

取り込んで基本的な動作確認

前述のCOSCUPでデモしたときのサンプルにできたJARを食わせて、ちゃんと動くことを確認しました。 おお、ここまでは結構順調。

github.com

jitpackにて仮公開

毎回毎回JAR差し替えるのは面倒くさいので、みんな大好きjitpackにて公開することにしました。

jitpack.io

こんな風にGitHubの公開リポジトリ登録するだけで、Mavenなどで参照できる形で公開してくれる上に、それを各バージョン管理システムmaven, gradle, sbtなど)でどう設定するかまで教えてくれる超便利サイトです。

f:id:naruoga:20210711171733p:plain
Jitpackでの公開画面。ブランチやタグ、コミットハッシュでも参照できるのが嬉しい

これで公開して、先のデモプログラムもそっちを参照するように(ローカルで)変えて、ちゃんと動くじゃーん、、と確認して、いい気持になって放置してたのが、ここまでのいきさつです。

ほぼ1か月の放置の末、大急ぎで続きをやろうというのが次の記事になります。あまり期待せずにお待ちあれ。結論を言うと、サンプル動いたくらいで安心してちゃいけなかったですw

*1:公式の宣伝サイト http://opendocumentformat.org が死んでいるっぽい……。

Ubuntu WilyのシステムPython3でLibreOfficeをコントロール

LibreOffice Advent Calendar 2015 17日目というか、Facebookの書き込みを見て、あれ、Ubuntuの場合はシステムPythonでPyUno(PythonからLibreOfficeの外部インタフェースUNOを使えるようにしたバインディング)使えたんじゃなかったっけと思って試してみました、というだけの話。

Pythonとか5行ぐらいしか書いたことないので(ややうそ)、勘違いとかあったら教えてください。

せっかくなので、ちょっとググっていたら見つかった、PyUNOをより簡単に使えるようにしたラッパーライブラリであるunotoolsを試してみます。

まずは必要そうなパッケージをaptでガツガツとインストール*1。すでに入ってるものもあるかもだけど気にしない。

$ sudo apt install libreoffice-script-provider-python python3 python3-uno

これでpython3からPyUNOが呼べることをまず確認します。

$ python3
Python 3.4.3+ (default, Oct 14 2015, 16:03:50) 
[GCC 5.2.1 20151010] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import uno
>>> 

問題なさげですね。

次にunotoolsですが、こいつはpipなライブラリなのでまずはpipを入れねばならんのです。apt searchするとpython3-pipというのがいるので、こいつだ!と思ったのですが、どうも罠っぽい。

qiita.com

あるべき姿がわからんのですが、調べるのもめんどくさいのでsetuptoolsを経由して入れてしまいます。virtualenvとか使う方がいいんだろうなーとか思いつつ。

$ sudo apt install python3-setuptools
$ sudo eazy_install3 pip
$ sudo pip install unotools

これで入ったっぽい。

ではunotools公式サイトの例題をやってみます。

最初にLibreOfficeをポート指定して起動。

soffice --accept='socket,host=localhost,port=8100;urp;StarOffice.Service'

それからpython3を起動し、パッケージをインポートしてコネクションを張り、Writerのインスタンスを起こす。

$ python3
Python 3.4.3+ (default, Oct 14 2015, 16:03:50) 
[GCC 5.2.1 20151010] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from unotools import Socket, connect                                        
>>> from unotools.component.writer import Writer
>>> context = connect(Socket('localhost', 8100))                                
>>> writer = Writer(context)  

で、起こしたWriterインスタンスに対して、「文字列を文章末に追加」する。

>>> writer.set_string_to_end('Hello\n')                                         
>>> writer.set_string_to_end('World\n')

はい、こんな感じでWriterにPythonから出力することができました。

f:id:naruoga:20151216210529p:plain

例のための例で恐縮ですが、unotools良さそうですね。

なお、本家PyUNOも、次期リリースの5.1では「もっとPythonらしいインタフェース」になるというお話が今年のLibreOffice Conferenceでありました。興味がある方は是非発表資料(PDF)をご覧あれ。

明日は誰か書いてくれるかな?それでは!

*1:最近、可能な限りaptコマンドで生きようと思っております。

デザパタを一人でこっそり振り返ろう #5 (Singleton)

なんと前の記事を確認したら3年近く前だよ…… orz

この連載?は、へっぽこプログラマー(厳密には足を洗ったので「元」)のぼくがひょんなきっかけから、Javaプログラマー向けデザインパターンの入門書として有名な:

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

(以下 JDP)を買ったはいいけどなんかやる気なくて放置してたところに、これまた Smalltalk 界隈の人に教えてもらった:

Design Patterns Smalltalk Companion, The (Software Patterns Series)

Design Patterns Smalltalk Companion, The (Software Patterns Series)

(以下DPSC)を買って読んだらむちゃくちゃ面白くて、じゃあ JDP の内容を Smalltalk で実装したあと、DPSC を答え合わせ的に読む、ということをやったら自分の勉強になるし、「動的言語におけるデザインパターンは静的言語のそれと違う」って意味が噛み締められるんじゃないかと思って始めたものです。

過去の記事は次のとおり。

ということで今回は Singleton パターン。言わずもがなではありますが、JDP から引用すると(p.58)、

指定したクラスのインスタンスが絶対に1個しかないことを保証したい

というものですな。これまた同じページから引用すると:

現在のシステム設定を表現したクラス、ウィンドウシステムを表現したいクラスなどが代表的な例

だそうです。ウィンドウシステムって、どのウィンドウもどっかからちゃんと手繰れないといけないんだけど(でないと、例えばマウスクリックとかのイベントを誰に渡せばいいんだかわかんなくなる)、そういうときに「全部のウィンドウの根っこ」とか持ちたくなるわけですね。そういうのをシングルトンで管理するよと。

そんなにインスタンスの作成を抑制したいんだったら、全部クラスメソッドでさばいてインスタンス作んなければいいんじゃね?と思うんですが、まあよくわかんないので教科書を追います。

ひとまず実装

JDP の例を見てみましょう。シングルトンであるクラス Singleton を作ります。

平たく言ってしまえば new を禁止して、代わりに「まだインスタンスがなければ new したものを、そうでなければすでにあるインスタンスを返す」メソッド getInstance を提供するってな感じです。Java の場合は new を禁止するにはコンストラクタ Singleton を private にすればいいわけですが、さて Smalltalk だとどうするか。private なんて Smalltalk にはねーよ。

まあ new 禁止を考えずにクラスの定義をば。


super new については前回 #4 で説明したとおり。ここで Singleton new ってやっちゃうと怒られる。……いや、これ self basicNew のほうがいいね。あとで気づいた。

でも自身の new 潰してないんだよなあ。ということで new にカーソル当てて alt-M でインプリメンタ出してなんかいいのないかなーと見てると、お、こんなんあった。

Bool class>>new
self error: 'You may not create any more Booleans - this is two-valued logic'

ふむ。self に error: で適切なエラーメッセージを投げてやればいいのね。じゃあこんなん?

そうすると「new 潰すと酷いことになるかも知んないけどマジいいの?」って聞かれるけどまあ構わずセーブ。

さてとワークスペース開けて試してみましょう。

a := Singleton getInstance.
b := Singleton getInstance.
a == b[print it]→true

ほい。うまくいったっぽい。

DPSC の解説

さて答え合わせ答え合わせ。DPSC の Singleton パターンは 91 ページから。ふむ。大筋は合ってるっぽい。new は self>>error: で潰せというのも正解でした。

Smalltalk 的にはシングルトンそのもの、あるいはシングルトンっぽいものはたくさん使われてるよって書いてあります。例えば Squeak の場合は Smalltalk 変数というのがいてコイツがグローバルな色々な何かを持ってたりしますが、このクラスである SmalltalkImage というクラスは new は「Smalltalk 使えよ」ってエラーを出すように潰してあります。

ということで、大抵のシングルトンパターンの場合は、システムそれ自体のグローバル変数とかに持っててそれ経由のアクセスをするのが普通なわけです。例えば上の例で示した Smalltalk 変数とかね。で、コイツの初期化はシステムのブートアップにやりますよと。でもシングルトンのオブジェクトってそれこそシステム根幹の情報なわけで、それをグローバル変数に持つってのはどうなのよ? 危なそうに見えるよね? と。

で、答えとしては、グローバル変数じゃなくてシングルトンオブジェクトのクラスに一つのメッセージを用意して、そいつからインスタンス変数にルーティングすればいいじゃんって。オブジェクトが生成されたとき、初期化されたとき、GCによって破棄された時など、クラスなら適切に扱えるじゃんねーと。なるほどね。でも実際のところ Smalltalk の大抵のシングルトンオブジェクトはグローバル変数で管理されてますよと。なんでやねん。

ちょっと英語苦手なんで実は意味読み取れてないんだけど引用:

Design Patterns states that Singletons accessed through global variables are not really example of the Singleton pettern (DP 127). One might argue that other examples are Singletons and they're just not implemented optimally.

デザインパターンではグローバル変数経由でアクセスされるシングルトンを Singleton パターン (DP 127) の実際の例とはしていません。一つ考えられることは、他の例はシングルトンなのですが、最適な実装がされていなかったということです。

つまるところ GoF ではシングルトンオブジェクトをグローバル変数に突っ込むのを「パターン」ではないとしてるけど、それっぽい例があるから「おーこうやって実装するといいねー」って最初実装しちゃったって話? そうなんかな。まあいいや。あ、ここで DP 127 っていうのはいわゆる Gang of Four (GoF) のデザインパターン

のページ 127 にあるパターンって意味です。はい。

とにかく本当はグローバル変数に突っ込むのは良いやり方ではないけど、Smalltalk ではそういう例がいっぱいあるってことはわかった。

シングルトンのバリエーション

シングルトンには persistent (永続)、transient (一時的)、single-active-instance (単独アクティブインスタンス) の三つのバリエーションがあるとのこと。

  • 永続シングルトン - 特定のクラスの唯一のインスタンスであり、なおかつ永遠に変わらないもの。
  • 一時的シングルトン - あるクラスの唯一のインスタンスだけども、インスタンスは変わりうるもの。例えばセッション情報を管理するオブジェクトはシングルトンであるが、セッションが破棄されると破棄され、新規にセッションが起きると再生成される。
  • 単独アクティブインスタンス - あるクラスに対し、アクティブまたは有効であるインスタンスは一つだけだが、インスタンス自体は複数存在するもの。例えば IDE のプロジェクトを管理するクラスがあるとして、IDE複数のプロジェクトをオープンできるのでインスタンス複数存在するけど、実際にプログラマーが弄ってるプロジェクトに対応するプロジェクトだけが有効とかそういう。実際にはシングルトンではないけど、性質的には似てるから一緒に論じますよと。
予約語

Smalltalknil, true, false とかは実は UndefinedObject, True, False という「クラス」のシングルトンオブジェクトとみなせますよと。実際 true をインスペクトすると True ってクラスのオブジェクトだよって言われるしね。

Smalltalk における実装について

Smalltalk 内部で実際に使ってるケースを紹介してくれるところが DPSC のうれしいところ。
GoF では Singleton パターンの要件として「(A)あるクラスがただひとつのインスタンスしか持たないこと」だけでなく、「(B)グローバルにアクセスできる方法を提供すること」を挙げているらしいです。ん? JDPにはその要件はスルーされていたような……。まあいいや。

単一インスタンスの保証

で、(A)はさっき議論したように、Smalltalkの場合はクラス変数 (Java の場合は静的メンバー変数っていうのかな)に作ったインスタンス放り込んでおいて new 禁止ってやり方でいいよねと。で、ついでにこんなふうな実装もありうるねと。これは Visual Smalltalk の UndefinedObject クラス。

UndefinedObject>>new
"レシーバーの新たなインスタンスを作る。このクラスでは単一のインスタンス nil が
存在するため許可されない。"
^self invalidMessage

で、invalidMessage を別途定義することで再利用性を上げると。ただ、VisualWorks とか Squeak とかこういうふうにしてなくて素朴に self>>error: 投げてる処理系も多いよと。

C++とかJavaの場合はnewをプライベートにするだけでできることなんだけど、すでにあるクラスを隠すって概念がないSmalltalkの場合は実行時例外を上げるしか方法がなく、文法レベルでチェックができないのは欠点だよねってことが書いてありました。まあそれは一理あるわなー。

インスタンスへのアクセスの提供

(B) についてですけど、さっきも書いたとおりグローバル変数を使うやり方は乱暴ではあるけど一応機能はする。もっと良いやり方はクラスでプロキシーする方法で、getInstance って名前はいかにも「インスタンスを貰う(貰った側が代入して持つ)」って名前でこれはちょっとよろしくない。ので、名前をcurrentとかに変えれば:

Singleton current someMessage.

みたいに自然に書けるでしょ? というお話でした。

new を潰さない方法ってどう?

よくよく考えてみると、newを潰す必要は別になくて、単にこうすればいいんじゃないかと。

Singleton class>>new
^self current

上の議論により self current はシングルトンオブジェクトを(必要なら内部で new して)返すんだから、これによって常に new で同じものが帰るよねと。

問題は、new って名前が「いかにもインスタンスを生成しそう」って名前なので:

| roadRunner wileECoyote |
roadRunner := SingleToon new.
wileECoyote := SingleToon new.
roadRunner position: 100@100.
wileECoyote position: 200@200.

で、実は roadRunner と wileECoyote が同じオブジェクトだとはお釈迦様でも気づくめえってことですわね。ので、やっぱ new は潰しておくほうが無難かなと。プログラマーが混乱したんじゃ意味ないんで。

クラス階層の中でのシングルトン

「このクラスのサブクラスの中のインスタンスは一個しかあっちゃダメ」みたいなことが欲しいときはどうするかって話ですね。Smalltalk の場合は一瞬で、クラス変数の代わりにクラスインスタンス変数を使えばいいですよと。つまり:

Object subclass: #Singleton
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'DPStudy-Singleton'

Singleton class instanceVariableNames 'Singleton'

とやれば後は同じでいいですね。っていうかクラスインスタンス変数ってこんなふうに使うんだー。

おっと、単なるテキストの引き写しになってる。いかんいかん。
その後も DPSC は「アクセサの名前 current とか default とかってどう決める?」とか、ぼくが最初に書いた「全部クラスメソッドでよくね?」とかそういう話があって、もっと具体的なコード例(データベースのラッパークラス)とかSmalltalkクラスライブラリの中での使用例とかいろいろ書いてあるわけでこれもまためちゃくちゃ面白い。が省略。買って読んで。

さて次回はいつになるかわかりませんがPrototypeパターンだそうです。お楽しみに。

はじめてのLibreOfficeソースコミット

2014.11.05 追記:とても重要な参考資料を紹介するの忘れていたので後ろにくっつけます。見てくださいね。


私は口先野郎なので、「LibreOfficeの日本コミュニティに一番必要なのはコード書く人。オープンソースはコード書く人が正義*1」とか言っていながら、自分はぜんぜんコード書いてなかったんですよね。これでも元はプログラミングでおまんまを食べてた人間として、これはいかんよなーってずっと考えてた。

開発に挑戦することに対してはいろんなハードルがあるだろうけど、LibreOfficeの場合はコードベースも巨大だしコミュニティもいろんな人が関わってるのでそれなりのお作法もある。正直、どこから手を付けていいかわかんないところがあります。で、私的には、もちろん開発そのものの難しさもあるけれども、それ以前に、とにかく自分の修正のパッチを送ってレビュープロセス通してって、人が介在するところに心理的に抵抗があったわけです。英語では丁寧にドキュメント化されてるのだけど、私英語得意じゃないしね。

で、これは誇っていいことだと思うんですが、LibreOfficeにはEasy Hacksという「開発入門者のための誰でも直せそうなバグ」をタグ付けしたものがあって、もうほんっとに簡単なバグ(で、緊急性が高くない奴)とかを元に、修正してコンパイルしてレビューシステムにpushしてレビュー受けて、ってことをできるようになってる。よし、これにチャレンジしようと。

LibreOffic Conference 2014 Bernのときから初めて、ついついサボってたのでこれはイケナイと思い立ち、自分の尻叩きのために関東LibreOfficeHackFest(#1) with 第119回東京エリアDebian勉強会というイベントをやって*2 ようやっと目鼻がつき、こないだ送ったパッチは無事に取り込まれました。

すっごいしょぼい修正で恥ずかしいのですが一応コミッター*3 の仲間入りをしたので、記録として残しておきます。

課題の選定

Easy HacksはAll LibreOffice Easy Hacks by required Skillというページで必要なスキルごとに分類されているんですが、今回は曲がりなりにも昔はそれでご飯食べてたC++の、超簡単レベルな奴からピックアップ。

Bug 43157 - Clean up OSL_ASSERT, DBG_ASSERT, etc.

Bug Descriptionから引用すると:

The assertion/logging functionality from osl/diagnose.h (OSL_TRACE, OSL_ASSERT, OSL_ENSURE, OSL_FAIL) and tools/debug.hxx (DBG_ASSERTWARNING, DBG_ASSERT, DBG_BF_ASSERT, DBG_WARNING, DBG_WARNING1--5, DBG_WARNINGFILE, DBG_ERRORFILE) is obsolete and needs to be cleaned up:

  • To assert invariants of the code (that can only be violated if there are programming errors) use standard C/C++ assert.
  • To log warnings about unusual events (that the code nevertheless needs to handle in some way, like malformed input or I/O failures), use the SAL_WARN... macros from sal/log.h.
  • To log other information useful for debugging, use the SAL_INFO... macros.

See https://wiki.documentfoundation.org/Development#Assertions_and_Logging, the mail thread at http://lists.freedesktop.org/archives/libreoffice/2011-November/020864.html, and the documentation in the sal/log.h header for further information.

要は古くて非推奨になったアサーション・ロギングマクロを、今のスタイルに書き直しましょう、ってだけ。機械的な置換でいけそうだからこれなら自分でもできるやろと。

ビルド環境構築

まずはgitでソースコードをまるっと取って来ます。で、ビルドに必要なライブラリをがっと取ってきて、一回ビルド通します。LibreOfficeのフルビルドは環境にも寄りますけど数時間かかるので、ビルド投入して寝ちゃうのがいいでしょう。

$ git clone git://gerrit.libreoffice.org/core LibreOffice
$ sudo apt-get build-dep libreoffice
$ cd LibreOffice
$ ./autogen.sh --enable-gstreamer --disable-gstreamer-0-10 --with-lang="ALL"
$ make

ビルド終わったらちゃんと動くことを確認します。

$ instdir/programs/swriter &

ヘルプ→LibreOfficeDevについて、を確認。

大丈夫そうですね。

コードの修正

今回の修正は色んなところのソースをお掃除しましょうというネタなので、どこ直してもまあいいわけです。なので直せそうなところをgrepで探しましょう。

豆知識としては、LibreOfficeはビルド時に依存するライブラリのバージョンを固定するために、サーバーにおいてあるtar ballを落としてきてworkdir/UnpackedTarballというところに展開します。その他yaccの生成物とかもあるので、単にgrep -r するとそういうのがいっぱい引っかかってしまうので、git grep使うほうが100倍ぐらい賢いです。
ということで、git grep OSL_ したら、あるわあるわ。どれ直してももちろんよいのですが、一部のソースは特定のconfigureオプションをオンにしないとコンパイルされない(そして、そのconfigureオプションはほとんど誰も使っていない)とかある*4 ので、ちょっとだけ注意が必要です。今回は pyuno/source/module/pyuno.cxx というソースを直すことにしました。変更が2行しかないのでw

ターゲットが決まったらまずは作業ブランチ切りましょう。

git checkout -b work

修正はバグ票にあった他のコミットを見ながら、こんな感じに書き換えればいいのかなと*5

--- a/pyuno/source/module/pyuno.cxx
+++ b/pyuno/source/module/pyuno.cxx
@@ -66,7 +66,7 @@ void PyUNO_del (PyObject* self)
 
 OUString val2str( const void * pVal, typelib_TypeDescriptionReference * pTypeRef , sal_Int32 mode )
 {
-    OSL_ASSERT( pVal );
+    SAL_WARN_IF( !pVal, "pyuno", "pVal != NULL" );
     if (pTypeRef->eTypeClass == typelib_TypeClass_VOID)
         return OUString("void");
 
@@ -124,7 +124,7 @@ OUString val2str( const void * pVal, typelib_TypeDescriptionReference * pTypeRef
         buf.append( "{ " );
         typelib_TypeDescription * pTypeDescr = 0;
         TYPELIB_DANGER_GET( &pTypeDescr, pTypeRef );
-        OSL_ASSERT( pTypeDescr );
+        SAL_WARN_IF( !pTypeDescr, "pyuno", "pTypeDescr != NULL" );
 
         typelib_CompoundTypeDescription * pCompType = (typelib_CompoundTypeDescription *)pTypeDescr;
         sal_Int32 nDescr = pCompType->nMembers;

で、make。フルビルド済ませてあるので、今度はそんなにかからないです。本当はmake checkでユニットテストも通すべき(そもそも、このコードにユニットテスト書くべき)なんですが、私が作業したときのmasterはなぜかmake checkすると通らない(よくあることのようです ^^;)ので、うーんと悩んだ末そのまま。

とりあえず、ローカルの作業ブランチにコミットしておきます。コミットコメントはこんな感じ。バグ修正のときはバグ番号を書くのが決まりです。

fdo#43157 - Clean up OSL_ASSERT, DBG_ASSERT
    
- Clean up OSL_ASSERT

gerritの設定

LibreOfficeでは、Googleが開発したソースコードレビューシステムGerritを使用しています。gerritの持つgitリポジトリにpushすると、その上でdiffを確認しつつ、レビューコメントがついたり質疑応答したりできるシステムです。ビルドbotも走っていて、そのコミットでビルドがぶっ壊れないかも確認してくれます。超便利。MLにパッチ投げて議論して、ってのもいいけど、gerritを使うほうが推奨されてる……はず。

ので、まずはgerritの設定するところから始めます。TDF WikiのGerritの解説ページを見ながら。

最初は https://gerrit.libreoffice.org にてアカウント作成ですが、これは大昔にやったことがあったので省略。Googleアカウントがあれば簡単だった記憶が。で、アカウント設定でgit push用のssh公開鍵を設定しておきます。もちろん専用の鍵ペア作ってもかまいません。

次に、解説ページによると、

You must set your username to match your freenode IRC nick (see 'Username' in your gerrit account settings).

とあるので、freenodeのNick登録しなきゃいけません。IRC技能が低い私はここでしばらく調べまわったのですが、自分 irssi 使ってるので、

/msg NickServ REGISTER <password> youremail@example.com

しといて、

/NETWORK ADD -autosendcmd "/^msg nickserv id <username> <password>;wait 2000" Freenode

でFreenodeのNickとGerritのアカウント名を合わせればいい……のかな。今んとこそうしてます。ただ、irssi常時上げてるわけじゃないし、irssiもnotify上げてくれるわけじゃないので実のところIRCマトモに使えてない。なんかいい方法ないすかね。ま、それはまた別の話。

で、先の解説ページに戻りまして、手動セットアップします。まずは.ssh/configに:

Host logerrit gerrit.libreoffice.org
       IdentityFile /path/to/your/private-key
       User <username>
       Port 29418
       HostName gerrit.libreoffice.org

を追記。秘密鍵のところは、さっきGerritに登録した公開鍵とペアになる鍵を指定しましょう。当たり前ですね。

で、LibreOfficeのソースディレクトリに移動しておいて、

$ ./loggerit test

とやって、"Your gerrit setup was successful!" と表示されればOK。

さてgitにpush先を指定しておきましょう。

$ git config remote.origin.pushurl ssh://logerrit/core

これで準備完了です。

いよいよpush……の前に

なんだかんだで、ソースコードを修正してからココらへんの設定をのたのたやってたので、まる2日ぐらい経ってたのかな?

LibreOfficeは進化が早いソフトウェアなので、2日も経つと当然masterには大量に変更が溜まってる。まあ、ビルド通らなくなるとは思えないけど、もし同じファイルを修正した人がいたりしたら悲しくなるので、念のためmasterを最新にして、自分の変更をその先頭にぶら下げるようにしたい。

私、git力低いので、ホントはこういうときってrebaseでできるのかもなあって思いつつ、悩んだ末masterをpullして最新にした後新たにブランチ切って、そこに作業ブランチの変更をcherry-pickするようにしました。いいのかなあ、こんなやり方で。

$ git checkout master
$ git pull origin master
<ぞろぞろとファイルが降りてくる>
$ git checkuout work
$ git log --oneline | head
<先頭は自分のコミットなのでハッシュ値を見ておく>
$ git checkout -b fdo43157 master
$ git cherry-pick <さっき確認したハッシュ値>
$ make
<ちゃんとmake通ってできたものが動くこと確認>

Gerritにpush

ドキドキしながらpush……してもいいのですが、いきなり投げるのはちょっと怖いので、なんか稽古場があった気がする……って読んだら、

<ローカルブランチ名>:refs/for/master の代わりに <ローカルブランチ名>:refs/drafts/master に投げるとドラフトとしてコミットできるよ

って書いてある。これはドラフトに一旦おいて議論するみたいな使い方をするところなのかなってのも思うのですが、まあ、突っ込んですぐ消すなら大丈夫かな、と……。

で、今の場合は:

$ git push origin fdo43157:refs/drafts/master

すると、「このURLに登録されたよ!」って出るので、そこを見に行くと、draftsにちゃんとコミットがあることが確認できる。

ホントは、このdraftsにadd reviewerしてレビュープロセスに移動するのが正しい使い方らしいのですが、誰をレビュアーにするとかプロセスがわかんなかったので、一度abandonして再度pushすることにしました。いいのかな。不安なので、みなさんはポリシー各自確かめてね。とにかくうりゃっとpush!

$ git push origin fdo43157:refs/for/master

すると、こんな感じで登録されます。あ、これレビューコメント色々もらって修正したやつなので最初とは変わってます。古いパッチも見られますけどね。最初にpushしたのはPatch 1。

pushしてしばらく経つと「ビルドbotが起動したよ」「ビルド成功したよ」とメールが来ます。ほっと一安心。あとは人間のレビュアーの登場を待ちます。

ライセンス宣言

おっと、ライセンス宣言もしないといけません。開発者・貢献者リストのページにあるとおり、開発者メーリングリストに:

All of my past & future contributions to LibreOffice may be
licensed under the MPLv2/LGPLv3+ dual license.

というメールを投げる必要があります。著作権譲渡や契約の締結などは不要です。めっちゃカジュアルですね。
本当は先の開発者リストWikiに自分の名前書かなきゃらしいのですが、このWiki超重要なので間違って編集したら怖いな、いわれたらやろうということで一旦保留*6

レビュー指摘反映

さて、ドキドキしながら待ってたら、コメントがつきました。まずはTakeshi Abeさん*7からの二つのコメント。

  • SAL_WARN_IFの第三引数は「こういう状態だからおかしいよ」ってものをログに残すものだから、条件逆じゃない?
  • SAL_WARN_IFの第二引数に指定するログエリアはinclude/sal/log-areas.doxに定義しないとダメだよ。

二番目の指摘は完全に見落としだったんですが、最初の指摘は……私はなにを考えていたんでしょう。まあ、勘違いとしかいいようがないですね。お恥ずかしや。

次の指摘はバグ#43157のreporterでもあるStephan Bergmannさんによるもの。

  • これ両方とも、NULLだったら後ろ死ぬからワーニングじゃなくてアサートにすべきじゃない?

……むぐ。そりゃそうだ。何を考えていたんでしょうワタクシ。

言い訳するとSAL_WARN_IFとかを定義してるinclude/sal/log.hxxにはアサートに当たるものがなかったのと、同じバグ票に対するこちらのコミットだとOSL_ASSERTをSAL_WARN_IFに書き換えているコードがあったからというのはあるんだけど、それはプログラマー的には思考停止であって言い訳にもならん。あかんわ。

で、間抜けな私はあれ、アサートのマクロってなくない?どれ使えばいいんだろ?と動揺して、日本語メーリングリストで質問したら、「いやいやC++のassert()でいいでしょう」ってCalcハッカーのKohei Yoshidaさんから教えてもらった。……あ。そうだね。そりゃそうだ。というか、バグ票にも " To assert invariants of the code (that can only be violated if there are programming errors) use standard C/C++ assert." って書いてあるじゃんかー。頭悪いな。

ということで、assert() に直した奴を再度コミット。--amend でコミットするとChange-Id:を見て、同じgerritエントリーに登録してくれるとな。素晴らしい。ので、それをpush。えいっ。これがPatch 2。

再びレビュー待ち……そして。

またビルドbotからのメールを受け取ってドキドキしながら待ってたら、「いいみたいだね、でもassert()使うなら #include を明示的にするほうがいいと思うな。この場合は他のヘッダでincludeされてるから問題ないけどね」って指摘をもらった。けど、それは別途直しておくよってことでパッチはacceptされて、無事コミットは取り込まれました。わーい!

http://cgit.freedesktop.org/libreoffice/core/commit/?id=922f2005f34589e60969be3f2bf74e4af58e2e69

感想

  • 仕組みはいろいろ良くできてる。ドキュメントが少し散らかってる感じはあるけど、ちゃんと読めばわかる。
  • Gerritすごく良くできてて感動。
  • みなさん大変親切でありがたや。お世話になりました。
  • 次はもうちょっと機能や不具合に関わる修正に挑戦したいな。



参考資料:Tomofumi Yagiさんの発表資料 (2014.11.05追記)

LibreOfficeへのコミットについては、Tomofumi Yagiさんのスライドが非常に参考になるので紹介しておきます。私が書かなかったOpenGrokなどについても説明されていて、必読です。

*1:あーもちろん、翻訳やアウトリーチ・プロモーションやその他の活動もやっぱり大事なんですよ。わかりやすいように極端な言い方をしてるだけで。

*2:タイトルの通り、東京エリアDebian勉強会の皆さんには大変お世話になりました。またコラボしましょう!

*3:LibreOfficeはgitで管理されており、誰でも1行からコミット可能なので、例えばApache OpenOfficeのように特定のコミッターという権限を持つ人がいるわけではありません。コードが取り込まれれば誰でもコミッターです。

*4:BernのHackNightで挑戦したのがうまく行かなかったのはこれが原因。

*5:あ、間違ってるってのは知ってるので、ちょっと待ってね。

*6:結局は、後述のStephenさんがやってくれました。

*7:LibreOffice日本語チームのメンバーで、アクティブにコミットされている安倍さん。