はじめに
以前の記事でlojを紹介しました。
LibreOfficeをJava帳票開発に使う(lojの紹介)
lojでは、LibreOffice UNOのインターフェースをJava Reflectionで解析し、P_Doc、P_Sheet、P_Cell、P_RangeなどのP_*クラスを自動生成します。
しかし、P_*クラスはUNO APIをJavaから扱いやすくするためのラッパーであり、それだけで帳票プログラムが十分に書きやすくなるわけではありません。
lojでは、その上にDoc、Sheet、Cell、Rangeなどの高水準APIを作ることを想定しています。
アプリケーション
↓
Doc / Sheet / Range / Cell
↓
P_Doc / P_Sheet / P_Range / P_Cell
↓
LibreOffice UNO API
↓
LibreOffice
loj-sb-sampleのloj.myパッケージには、そのサンプルとなる高水準APIを入れています。
この記事では、
-
P_*クラスの使い方 -
loj.my.Utilの使い方 - 高水準APIの書き方
の順に説明します。
まずはP_*クラスの使い方
P_*クラスはloj-proxyによって自動生成されるUNOラッパーです。
例えば、
P_Doc
P_Sheet
P_Range
P_Cell
P_Chart
などがあります。
UNOを直接Javaから利用すると、目的のインターフェースを取得するために、
XSomething xSomething =
UnoRuntime.queryInterface(
XSomething.class,
object
);
のような処理を頻繁に書くことになります。
P_*クラスでは、このようなUNOインターフェースを取得する処理をラッパーの中へ隠しています。
そのためアプリケーション側では、生成されたJavaオブジェクトとしてUNOを操作できます。
P_*はUNOに近いAPI
ここで重要なのは、P_*は高水準APIではないということです。
UNO
↓
P_*
という非常に薄い層です。
つまり、
UNOの機能を別のAPIに置き換える
のではなく、
UNOをJavaから書くときの機械的な処理を減らす
ためのクラスです。
したがってUNO APIの知識はそのまま利用できます。
prop()でプロパティを設定する
LibreOffice UNOではXPropertySetによるプロパティ操作が非常によく使われます。
XPropertySetを持つP_*クラスでは、prop()を使ってプロパティを設定できます。
例えば、
range.prop("CellBackColor", 0xAAAAAA);
さらにprop()は自分自身を返すので、
range
.prop("CellBackColor", 0xAAAAAA)
.prop("CellStyle", "MyStyle");
のように続けて書けます。
帳票では背景色、罫線、フォント、表示形式など多数のプロパティを設定するため、この書き方はかなり便利です。
必要ならUNOへ戻る
P_*クラスですべてを隠しているわけではありません。
getProxy()を使えば、ラップしているUNO Objectを取得できます。
Object obj = sheet.getProxy();
そのため、
通常
↓
P_*を使う
P_*で足りない
↓
getProxy()
↓
UNO APIを直接使う
という使い分けができます。
次にloj.my.Utilの使い方
loj-sb-sampleのloj.my.Utilには、UNOを利用するときに便利なユーティリティメソッドをまとめています。
大きく分けると、
- UNOを使ったプログラムで通常使用するユーティリティ
- UNOオブジェクトのInterface、Service、Propertyなどを調査するためのユーティリティ
の2種類があります。
通常使用するユーティリティ
まず、UNOを使ったプログラムで通常使用する3つのメソッドを紹介します。
qi()
public static <T> T qi(Class<T> inteface, Object object) {
return UnoRuntime.queryInterface(inteface, object);
}
UnoRuntime.queryInterface()の省略形です。
UNOをJavaから利用すると、
XPropertySet ps =
UnoRuntime.queryInterface(
XPropertySet.class,
object
);
というコードを頻繁に書きます。
Util.qi()を使えば、
XPropertySet ps =
Util.qi(XPropertySet.class, object);
と書けます。
ほんの少しの違いですが、UNOではqueryInterface()を非常によく使うので、コードを短くできます。
qiという名前はqueryInterfaceの略です。
getPropertyValueArray()
public static PropertyValue[] getPropertyValueArray(
Map<String, Object> map)
Map<String, Object>をUNOのPropertyValue[]へ変換します。
UNOでは、メソッドの引数としてPropertyValue[]を要求されることがよくあります。
getPropertyValueArray()を使えば、Map.of()から簡単に生成できます。
PropertyValue[] props =
Util.getPropertyValueArray(
Map.of(
"Hidden", true,
"ReadOnly", false
)
);
UNOでは本来、
PropertyValue pv1 = new PropertyValue();
pv1.Name = "Hidden";
pv1.Value = true;
PropertyValue pv2 = new PropertyValue();
pv2.Name = "ReadOnly";
pv2.Value = false;
PropertyValue[] props = {
pv1,
pv2
};
のように一つずつPropertyValueを生成する必要があります。
lojでは、
Util.getPropertyValueArray(
Map.of(
"Hidden", true,
"ReadOnly", false
)
);
と書けます。
getColor()
public static int getColor(String cs)
HTML/CSS風の16進カラー文字列を、UNOで利用する整数値へ変換します。
例えば、
int color = Util.getColor("#AAAAAA");
とすると、
0xAAAAAA
相当の整数値になります。
そのため、
cell.prop(
"CellBackColor",
Util.getColor("#AAAAAA")
);
のように書けます。
Javaコード上でも、
#FF0000
#00FF00
#0000FF
のような馴染みのあるRGB表記で色を指定できます。
不正な文字列やnullの場合は0を返します。
UNOを調査するユーティリティ
UNOは非常に巨大なAPIです。
プログラムを書いていると、
このObjectはどのUNO Interfaceを持っているのか?
どのServiceをサポートしているのか?
どんなPropertyを持っているのか?
を調べたいことがあります。
そのためUtilには、実際のUNOオブジェクトを調査するためのメソッドも用意しています。
getTypeProviderStr()
public static String getTypeProviderStr(Object o)
UNOオブジェクトが持っているUNOの型・Interfaceを一覧にして返します。
内部ではXTypeProviderを取得して、
tp.getTypes()
から利用可能な型を取得し、型名でソートして文字列にします。
例えば、
System.out.println(
Util.getTypeProviderStr(object)
);
とすれば、
このUNO ObjectからどのInterfaceを取得できるのか?
を調べられます。
また、P_*などProxiableを実装したlojのオブジェクトを渡した場合には、自動的にgetProxy()して中のUNO Objectを調べます。
そのため、
System.out.println(
Util.getTypeProviderStr(pSheet)
);
のようにP_*オブジェクトをそのまま渡すこともできます。
writeType()
public static void writeType(
String filename,
Object o,
String comment)
getTypeProviderStr()で取得したUNOの型情報をファイルへ追記します。
例えば、
Util.writeType(
"uno-types.txt",
object,
"Sheet"
);
とすると、
#Sheet
=...
という形式で型情報がファイルへ追加されます。
複数のUNOオブジェクトについて、
Util.writeType("types.txt", doc, "Document");
Util.writeType("types.txt", sheet, "Sheet");
Util.writeType("types.txt", cell, "Cell");
のように記録しておけば、それぞれがどのInterfaceを持っているのかを後から比較できます。
getServiceInfoStr()
public static String getServiceInfoStr(Object o)
UNOオブジェクトのService情報を取得します。
内部ではXServiceInfoを取得して、
getImplementationName()
getSupportedServiceNames()
を呼び出しています。
例えば、
System.out.println(
Util.getServiceInfoStr(object)
);
とすると、そのオブジェクトの実装名と、サポートしているUNO Serviceの一覧を確認できます。
UNOでは、
Interface
Service
Implementation
という概念が出てくるため、
このオブジェクトは実際には何のServiceなのか?
を確認したいときに便利です。
これもProxiableに対応しているため、
System.out.println(
Util.getServiceInfoStr(pSheet)
);
のようにP_*オブジェクトを直接渡せます。
getPropsStr()
public static String getPropsStr(Object o)
UNOオブジェクトが持っているPropertyの一覧と、現在の値を取得します。
内部ではXPropertySetを取得し、
ps.getPropertySetInfo().getProperties()
ですべてのPropertyを調べ、それぞれについて、
ps.getPropertyValue(p.Name)
を実行しています。
例えば、
System.out.println(
Util.getPropsStr(cell)
);
とすれば、セルが持っているPropertyと現在値を一覧できます。
LibreOfficeでは、例えばセルの書式だけでも、
CellBackColor
CharColor
CharHeight
NumberFormat
HoriJustify
VertJustify
TopBorder
BottomBorder
...
など多数のPropertyがあります。
UNOのドキュメントを調べるだけでなく、実際のオブジェクトに対してgetPropsStr()を使うことで、
このオブジェクトには実際にどんなPropertyがあるのか?
を確認できます。
getPropsStr()もProxiableに対応しているため、lojのP_*やその高水準クラスをそのまま渡せます。
調査系メソッドの使い分け
調査系のメソッドは、次のように使い分けられます。
| メソッド | 調べられるもの |
|---|---|
getTypeProviderStr() |
UNOの型・Interface |
writeType() |
型・Interfaceをファイルへ記録 |
getServiceInfoStr() |
Implementation・Service |
getPropsStr() |
Propertyと現在値 |
例えば、正体のよく分からないUNO Objectを取得したら、
System.out.println(Util.getTypeProviderStr(o));
System.out.println(Util.getServiceInfoStr(o));
System.out.println(Util.getPropsStr(o));
としてみます。
すると、
このUNO Objectは何者?
↓
どんなInterfaceを持っている?
↓
どんなService?
↓
どんなPropertyを持っている?
と実際のオブジェクトを調査できます。
UNOのドキュメントだけを読んで高水準APIを考えるのではなく、実際のLibreOfficeオブジェクトを調べながら必要な機能を見つけ、高水準APIにしていくための道具として使えます。
最後に高水準APIの書き方
ここからがlojで一番自由な部分です。
自動生成されたP_*クラスを直接使ってもLibreOfficeを操作できますが、帳票プログラムではさらに高水準のクラスを作るとコードが読みやすくなります。
loj-sb-sampleでは、
P_Doc → Doc
P_Sheet → Sheet
P_Range → Range
P_Cell → Cell
という高水準APIを用意しています。
UNOの操作ではなく帳票の操作を書く
例えばセルを取得して値を設定する処理を考えます。
低水準のAPIでは、
セルオブジェクトを取得
↓
必要なUNOインターフェースを取得
↓
値を設定
というUNO側の都合がコードに現れます。
高水準APIでは、
Cell cell = sheet.getCell("A1");
cell.set("ABC");
と書けるようにします。
つまり高水準APIでは、
UNOをどう操作するか
ではなく、
帳票で何をしたいか
をメソッドにします。
よく使う操作をメソッドにする
高水準APIでは、UNOを使って実現した処理を、帳票を書くときに使いやすいメソッドとしてまとめます。
例えばCellには、現在のセルから相対位置にあるセルを取得するoffset()があります。
実際のコードは次のようになっています。
public Cell offset(int cOff, int rOff) throws IndexOutOfBoundsException {
var a = getCellAddress();
return new Cell(
getSpreadsheet().getCellByPosition(
a.Column + cOff,
a.Row + rOff
)
);
}
まずgetCellAddress()で現在のセルの位置を取得します。
そして、
a.Column + cOff
a.Row + rOff
で移動先の座標を計算し、UNOのgetCellByPosition()でそのセルを取得して、新しいCellとして返しています。
例えばA1から右へ2列、下へ3行移動したセルを取得するなら、
Cell cell = sheet.getCell("A1");
Cell target = cell.offset(2, 3);
と書けます。
この場合、targetはC4のセルになります。
A1
│
│ offset(2, 3)
│
├── 右へ2列
│
└── 下へ3行
↓
C4
このoffset()を使えば、上下左右への移動も簡単に実装できます。
実際のCellでは次のようにしています。
public Cell up(int rOff) throws IndexOutOfBoundsException {
return offset(0, -rOff);
}
public Cell down(int rOff) throws IndexOutOfBoundsException {
return offset(0, rOff);
}
public Cell left(int cOff) throws IndexOutOfBoundsException {
return offset(-cOff, 0);
}
public Cell right(int cOff) throws IndexOutOfBoundsException {
return offset(cOff, 0);
}
さらに1セルだけ移動する場合のために、引数なしのメソッドも用意しています。
public Cell up() throws IndexOutOfBoundsException {
return up(1);
}
public Cell down() throws IndexOutOfBoundsException {
return down(1);
}
public Cell left() throws IndexOutOfBoundsException {
return left(1);
}
public Cell right() throws IndexOutOfBoundsException {
return right(1);
}
これによって、
Cell cell = sheet.getCell("B2");
Cell right = cell.right();
Cell down = cell.down();
のように、セルの位置を意識したコードを書けます。
また、set()もCell自身を返すため、
Cell c = sheet.getCell("A1");
c.set("JAN")
.right().set("FEB")
.right().set("MAR");
のような書き方もできます。
このように、
UNOで何度も書く処理を小さなメソッドにまとめ、そのメソッドを組み合わせて、帳票の操作として読めるAPIにする
のが、lojで高水準APIを書く基本的な考え方です。
P_*を土台にする
高水準APIですべてのUNO処理を一から実装する必要はありません。
基本的には、
LibreOffice UNO
↓
loj-proxyで自動生成
↓
P_*
↓
必要な処理だけ手書き
↓
Doc / Sheet / Range / Cell
とします。
機械的に作れるUNOラッパーはloj-proxyに作らせ、人間は帳票で意味のあるAPIを書く、という役割分担です。
高水準APIは自分で育てる
loj.myの高水準APIは、完成されたAPIとして固定することを目的としていません。
loj-sb-sampleから必要なクラスをコピーして、自分の帳票システムで必要なメソッドを追加していくことを想定しています。
例えば、
cell.currency();
cell.percent();
cell.border();
のような操作が頻繁に必要なら、自分のCellに追加して構いません。
あるいは、
sheet.createHeader(...);
sheet.createDetail(...);
sheet.createFooter(...);
のように、さらに業務寄りのAPIを作ることもできます。
すると、
UNO
↓
P_* ← 自動生成
↓
Doc / Sheet / Cell ← 共通の高水準API
↓
帳票固有API ← 利用者が追加
↓
業務コード
と段階的に抽象度を上げられます。
これがlojで高水準APIを書く基本的な考え方です。
まとめ
lojでは、すべてを一つのライブラリで隠蔽するのではなく、役割を分けています。
LibreOffice UNO API
↓
P_*クラス
│
│ loj-proxyによる自動生成
↓
Util
│
│ UNOで頻出する処理を補助
↓
Doc / Sheet / Range / Cell
│
│ 人間が使いやすいAPI
↓
アプリケーション
P_*ではUNOをJavaから扱う煩雑さを減らし、UtilではUNO特有の細かい処理をまとめ、その上に自分の用途に合った高水準APIを作ります。
高水準APIは最初から完璧なものを作る必要はありません。
帳票を書いていて、
この処理、何度も書いているな
と思ったものをCell、Range、Sheet、Docなどへ少しずつ追加していけばよいと思います。
機械的なUNOコードはloj-proxyに書かせ、人間は帳票の意味を表すコードを書く。
これがlojの高水準APIの基本的な考え方です。