動作の説明
内部で利用しているクラス
最初にOreProg.javaの中で利用しているクラスについて簡単に説明します。
ハンドラクラス
OreProg.javaのstatic内部クラスとして実装されています。SAXパーサのコンテントハンドラ(org.xml.sax.ContentHandler)の実装ですが、基底クラスとしてorg.xml.sax.helpers.DefaultHandlerを利用しています。
Expression実装クラス
oreprogを実行するインタープリタです。OreProgのstatic内部クラスおよび外部のJavaソースファイルとして実装しています。なお、Expressionそのものはインターフェイスです。
XMLの読み込みと構文木の作成
SAXの利用
JavaでSAXを利用する場合、現在はここで利用しているorg.xml.sax.XMLReaderを利用するのが良いでしょう。XMLReaderの良い点は、setContentHandlerメソッドを利用することで、動的にContentHandlerの切り替えが可能な点です。
public void read(InputStream stm) throws SAXException, IOException { final XMLReader r = XMLReaderFactory.createXMLReader(); r.setContentHandler(new SourceHandler() { { reader = r; } public void startElement( String uri, String localName, String qName, Attributes attrs) throws SAXException { if (!qName.equals(getRootElementName())) { super.startElement(uri,localName,qName,attrs); } } protected void addExpression(Expression exp) { program.add(exp); } public void endElement( String uri, String localName, String qName) throws SAXException { } }); r.parse(new InputSource(stm)); }
ここでSourceHandlerを継承した無名内部クラスを生成して与えているのは、作成されたExpression(インタープリタの実装)をprogramリストに格納するためです。ハンドラクラスはOreProgの実装には依存していないため、このような方法でルート要素で作成されたExpressionをOreProgのインスタンスに取り込んでいます。
XMLReaderを格納している変数をfinalで修飾しているのは無名内部クラスから参照するためです。各ハンドラは、子要素の開始時に適切なハンドラを生成して、XMLReaderに登録します。そのため、XMLReaderの参照が必要となります。
XMLReader#parseの呼び出しで実際のパーシングが開始されます。
SourceHandlerの役割は、SAXから与えられた要素名によって担当するハンドラを生成して制御を与えることと、各ハンドラによって生成されたExpressionをまとめていくことです。
protected static class SourceHandler extends DefaultHandler { XMLReader reader; SourceHandler parent; Expression expression; /** * ノードを設定する。 */ protected void setExpression(Expression newExpression) { expression = newExpression; } /** * 初期設定を行う。オーバーライドする場合は * 必ず最初に親クラスの呼び出しを行う。 * @param initReader 現在パーシングに利用しているXMLReader * @param initParent 親要素ハンドラ * @param attrs この要素の属性 */ protected void initialize(XMLReader newReader, SourceHandler initParent, Attributes attrs) throws SAXException { reader = newReader; parent = initParent; } /** * 子要素が作成したノードを受け取る。 * 基底クラスはSAXExceptionをスローする。 * @param exp 作成したノード */ protected void addExpression(Expression exp) throws SAXException { throw new SAXException("no enclosing elements: " + exp); } public void startElement( String uri, String localName, String qName, Attributes attrs) throws SAXException { SourceHandler h; HandlerFactory fac = (HandlerFactory)handlers.get(qName); if (fac == null) { if (RootObject.isFunction(qName)) { h = new RootHandler(qName); } else { throw new SAXException("unknown element:" + qName); } } else { h = fac.create(); } h.initialize(reader, this, attrs); reader.setContentHandler(h); } /** * 保持している{@link OreProg.Expression}を参照するので、 * オーバーライドする場合は最後に呼び出すこと。 */ public void endElement( String uri, String localName, String qName) throws SAXException { parent.addExpression(expression); reader.setContentHandler(parent); } }
ハンドラ切り替えの制御に利用しているstartElementメソッドとendElementメソッドはorg.xml.sax.ContentHandlerで定義されているSAXのイベント受信メソッドです。この方法によりXMLの要素がネストされている場合、その親子関係をそのままハンドラの親子関係としてたどっていきます。別の方法論として、スタックを利用する方法もありますが、筆者はstartElementで子ハンドラを登録し、endElementで親ハンドラへ制御を戻すここでの手法が好きです。
SourceHandlerのその他のメソッドは、親要素が保持するExpressionに子要素を追加するためのaddExpressionと、派生クラスが生成したExpressionを受け取るためのsetExpressionメソッドです。この2つは良く似た名称ですが、子要素からの通知と自要素からの通知と、意味がまったく異なるので注意してください。
SourceHandlerは実行時には、startElementの呼び出しで子要素ハンドラを生成し初期化のためにinitializeメソッドを呼び出し、endElementの通知を受けるとaddExpressionメソッドを呼び出して作成したExpressionを親ノードに登録します。またXMLReaderに対して親ハンドラを登録することで制御を親ハンドラへ戻します。派生クラスはinitializeとaddExpressionの2つのメソッドをオーバーライドすることとなります。
ContentHandlerの派生クラスからの拡張
ここで設計時に考慮すべき点として、OreProg派生クラスからの構文追加やOreProg自身の構文拡張を容易にするには、oreprogで利用する要素を自由に追加できるようにすべきだということが挙げられます。そのためには、SourceHandlerが生成するハンドラを要素名から直接的に参照しなくてもすむようにしなければなりません。
if (qName.equals("if")) { // ifという要素名を知っている必要がある。 h = new IfHandler(); // IfHandlerクラスを知っている必要がある。 }
// qName(要素名)の内容を知る必要がない
HandlerFactory fac = (HandlerFactory)handlers.get(qName);
OreProgの実装では、SourceHandlerが要素名の知識を持たずに済ませられるように、要素名とハンドラの関連をMapで管理します。この時、Mapに直接ハンドラクラスのインスタンスを格納できれば話は簡単ですが、それは無理です。なぜならば、ハンドラクラスはインスタンスフィールドを利用するため(親ハンドラや生成中のExpressionを保持する必要があります)、Mapに格納した単一のインスタンスでXML上に現れたすべての要素を処理することができないからです。そのためOreProgではMapに直接ハンドラクラスのインスタンスを保持するかわりに、Abstract Factoryパターンのインスタンスを利用しています。実装方法としては、HandlerFactoryというファクトリインターフェイスを規定し、そのインターフェイスを実装したファクトリクラスのインスタンスをMapへ登録するという手法を採用しています。
protected interface HandlerFactory { SourceHandler create(); } static Map handlers = new HashMap(); static { handlers.put("if", new HandlerFactory() { public SourceHandler create() { return new IfHandler(); }}); handlers.put("condition", new HandlerFactory() { public SourceHandler create() { return new ConditionHandler(); }}); handlers.put("block", new HandlerFactory() { public SourceHandler create() { return new BlockHandler(); }}); ...
このような単純なインターフェイスについては個別にクラスを定義せずに、ここでの例のように無名内部クラスとしてインスタンス化したほうが良いでしょう。
なお、ここではhandlers Mapを参照するのがOreProg内部のSourceHandlerのみだという前提で直接staticフィールドのMapを参照させていますが、必要性があれば独立させて一般的なAbstract Factoryクラスとして実装することもできるでしょう。
// この場合はinterfaceの定義は不要 public abstract class HandlerFactory { static Map handlers = new HashMap(); public abstract SourceHandler create(); public static HandlerFactory getInstance(String elementName) { return (HandlerFactory)handlers.get(elementName); } protected static addHandlerFactory( String elemName, HandlerFactory fac) { handlers.put(elemName, fac); } }
初期化ブロック
handlers Mapの初期化に利用しているstatic { }という記述を「初期化ブロック(initialization block)」と呼びます。この例のようにstaticで修飾している初期化ブロックはクラスロード時に実行されます。
初期化ブロック内ではfinalフィールドへの代入もできますので、この例で示されているMapのように、複数の文を記述しないと初期化できないコレクションの初期化に利用すると良いでしょう。
特に、読み込み専用のコレクションをstatic finalフィールドに持ちたい場合には、クラスが利用される前にコレクションへの書き込みを禁止する必要があるため
static final Map fixedMap; static { Map m = new HashMap(); m.put("..", ".."); ... fixedMap = Collections.unmodifiableMap(m); }
のように、、初期化ブロックを利用しなければ実現できません。
ちなみに、初期化ブロックは非staticのものも記述でき、こちらはコンストラクタの呼び出し前に実行されます。インスタンス初期化フィールドは既に見たOreProg#readで無名内部クラスのreaderフィールドの初期化に利用しています。
r.setContentHandler(new SourceHandler() { { reader = r; } // 初期化ブロック public void startElement(String uri, String localName, String qName,Attributes attrs) throws SAXException {
SAXパーサでの文字データ処理
SAXパーサ(XMLreader)の利用時に気をつけるべき点として、ContentHandler#charactersメソッドの実装があります。長い文字データが続く場合、XMLReaderは要素の途中でConentHandlerを呼び出す場合があるからです。そのため、バッファリングをしてendElement呼び出し時に展開するほうが安全です。
ValueHandlerは、value属性に設定値を記述することを想定していますが、文字データとして設定することも可能なように設計してあります。
static class ValueHandler extends SourceHandler { ... String value; StringBuffer charBuffer; protected void initialize(XMLReader newReader, SourceHandler initParent,Attributes attrs) throws SAXException { ... value = attrs.getValue(VALUE); // 最初は属性からvalueを設定 ... } public void characters(char[] ch, int start, int length) throws SAXException { if (charBuffer == null) { charBuffer = new StringBuffer(); } charBuffer.append(ch, start, length); } public void endElement( String uri, String localName, String qName) throws SAXException { setExpression(createExpression()); super.endElement(uri, localName, qName); } protected Expression createExpression() throws SAXException { Expression ex; if (charBuffer != null) { // 文字データから設定 value = new String(charBuffer).trim(); } .... } }
valueフィールドの値は通常はvalue属性で設定されるため、charBufferフィールドはnullのままです。しかし、charactersメソッドの呼び出しが行われるとStringBufferを設定し、与えられた文字配列をバッファリングしていきます。
最後、endElementから呼び出されたcreateExpressionメソッド内で、charBufferがnullかどうかチェックされ(文字データが無ければcharactersメソッドは呼び出されないため、nullのままなので)インスタンスが設定されていればそこから文字列を取り出してvalueフィールドへ設定します。なお、trimメソッドを適用して前後の空白を削除していますが、この動作が正しいかどうかはXMLの記述方法に依存します。
ここでは、
<value type="string"> ここに文字列を記述。 </value>
と、文字データの記述にインデントが適用されることを想定しているためtrimメソッドを呼んでいますが、前後の空白が重要な場合には、
<value type="string"> 先頭に空白がある文字列</value>
と記述するように、あらかじめ記述規約を決めておかなければなりません。XMLに文字データを含める場合、プログラムでは後者のように記述されていることを暗黙の前提としたため、前者のように記述されて間延びした表示になってしまうなどの運用上の問題が出てくることがあるので注意が必要です。
プログラムの実行
実行のためメソッド
oreprogは、各Expressionインターフェイスのevaluateメソッドの連鎖によって実行されます。
/** * 構文木のノードが実装するインターフェイス。 */ public interface Expression { /** * 現在のコンテキストに従って評価する。 * @param context 評価に利用するコンテキスト * @return 評価後の戻り値 */ Object evaluate(Map context) throws Exception; }
引数のMapは、実行時に参照したり生成したりした変数が格納されます。
実行の開始
oreprogの実行開始はOreProg#executeメソッドが担当します。
/** * プログラムを実行する。 * @param args 実行パラメータ(プログラム内ではargsという名前でアクセスできる) * @return 終了時の実行コンテキスト */ public Map execute(List args) throws Exception { Map context = new HashMap(); context.put(ROOT_OBJ, new RootObject()); context.put(ARG_OBJ, args); BlockExpression e = new BlockExpression(program); e.evaluate(context); return context; }
最初に実行時の変数を格納するためのMapを用意しておきます。このMapには、組み込み変数として引数のList(変数名はarg)と、関数を実現するオブジェクトのインスタンスを格納します。
executeメソッドは単純なメソッドなのであらかじめ多数の組み込み変数や組み込みオブジェクトを用意する場合には直接オーバーライドしても良いし、Mapの生成にFactory Methodパターンを適用しても良いでしょう。
Factory Methodパターンを適用した場合、executeメソッドは以下のように変更されます。
public Map execute(List args) throws Exception { Map context = createContext(); BlockExpression e = new BlockExpression(program); e.evaluate(context); return context; } protected Map createContext() { Map context = new HashMap(); context.put(ROOT_OBJ, new RootObject()); context.put(ARG_OBJ, args); return context; }
このようにしておくと派生クラスで、
protected Map createContext() { Map context = super.createContext(); context.put("my-greeting", "Hello World !"); context.put("my-farewell", "More Light !"); return context; }
のようにオーバーライドによって組み込み変数の拡張などが容易になります。
OreProg#executeメソッドは、Mapの準備が完了するとBlockExpressionのインスタンスを生成して呼び出しを依頼します。BlockExpressionはifやwhileでのブロック(Javaだと{})を担当する構文要素で、複数のExpressionを順次実行します。
static class BlockExpression implements Expression { List exps; BlockExpression(List initExps) { exps = initExps; } public Object evaluate(Map context) throws Exception { Object o = null; for (Iterator i = exps.iterator(); i.hasNext(); ) { Expression e = (Expression)i.next(); o = e.evaluate(context); context.put("$?", o); } return o; } }
各Expressionの評価後に結果を$?という変数名でMapに格納しています(「sample.xml」などではこの変数は参照していません)。
ちなみに、説明が前後しますが、BlockExpressionを生成するハンドラは実装が単純なのでハンドラクラスのサンプルとしては理解しやすいでしょう。
static class BlockHandler extends SourceHandler { List exps = new ArrayList(); protected void addExpression(Expression exp) { exps.add(exp); } public void endElement( String uri, String localName, String qName) throws SAXException { setExpression(new BlockExpression(exps)); super.endElement(uri, localName, qName); } }
BlockHandlerは子要素が作成したExpressionをaddExpressionメソッドで受け取るとListに追加していきます。ブロックの実行順序はXMLの要素の出現順と同じなのでこれで問題ないわけです。最後endElementの呼び出し時にBlockExpressionのインスタンスを生成してフィールドに設定します。ここで設定したBlockExpressionのインスタンスは、super.endElementの呼び出し内で親ハンドラに対するaddElement通知で利用されることになります。
まとめ
XMLを利用して、Interpreterパターンのインスタンスを比較的容易に生成できます。ここではインタープリタとして実装しましたが、コンパイラとして実装することもできます。コンパイラでは必ずしもアセンブリを出力する必要はありません。動的に実行するのではなく、静的にプログラムを生成すれば良いわけなので、出力をJavaのソースファイルとしても良いのです。このような手法によりXMLを利用した「DSL(ドメイン特化言語)」を作ることもできるでしょう。
この時、XMLの生成部分をグラフィカルなツールとして実装するなど工夫をすれば、いろいろな活用方法が生まれます。
XMLは文書としてインスタンス化されれば、単なる設定ファイル以上の使い方がいろいろ可能となる不思議な存在です。また、プログラミング言語を実際に作ってみることはプログラミングの良い勉強になります。
本記事のサンプルソースは自由に使ってかまいません。いろいろ試してJavaの学習などに利用してみてください。
練習問題
oreprogの関数(addやprint)は、OreProg.javaのstatic内部クラスとして実装したRootObjectクラスによって提供しています。oreprogの構文は派生クラスから拡張可能なように構成されていますが、関数についてはOreProgがRootObjectを固定的に利用しているために派生クラスからの拡張ができません。
問題1:
OreProgを修正しないで、print関数の動作を改行しないように変更し、代わりに改行を行うprintln関数を追加してください。
問題2:
OreProgを修正してRootObjectを継承することで関数の追加を可能にしてください。
練習問題の解説
問題1:
回避方法の探索問題です。printおよびprintlnを関数ではなく構文要素にすることでOreProgの変更を回避してこれらを実装することができます。
OreProg.SourceHandler#startElementでは関数の判定を構文要素の判定の次に実行しています。
HandlerFactory fac = (HandlerFactory)handlers.get(qName); if (fac == null) { if (RootObject.isFunction(qName)) { h = new RootHandler(qName); } else { throw new SAXException("unknown element:" + qName); } } else { h = fac.create(); }
したがって、printについては要素のハンドラを外部から与えることでRootObjectの呼び出しを回避することができます。
しかし、構文要素の実装はHandlerクラスとExpressionクラスの2つのクラスの実装が必要となることから、単純な関数の追加には望ましい方法とは言えません。
逆の言い方をすると、問題2の解答はこの事実(代替手段がある)を踏まえた上でより柔軟な方法を採用する必要があるということになります。
問題2:
論点は3カ所あります。最初はRootObjectのアクセス指定子です。1つは、継承したRootObjectをOreProgに利用させることです。もう1つは、追加したメソッドをSourceHandlerに認識させるための方法です。
最初の点については、RootObjectのアクセスをパッケージプライベートからprotectedに変更します。なお、Javadocコメントをきちんと付ける場合には、protectedアクセスのクラスについてはデフォルトで出力対象となるため、ドキュメントを入れたほうが良いでしょう。
2番目はいくつかの方法が考えられますが、ここでは派生クラスによる元のクラスが利用するオブジェクトの入れ替えですからFactory Methodパターンを適用するのが良いでしょう。
RootObjectの生成は、OreProg#execute内で実行しているので、該当箇所を以下のように修正します。
public Map execute(List args) throws Exception { Map context = new HashMap(); - context.put(ROOT_OBJ, new RootObject()); + context.put(ROOT_OBJ, createRootObject()); context.put(ARG_OBJ, args); BlockExpression e = new BlockExpression(program); e.evaluate(context); return context; }
そして次のファクトリメソッドを追加します。
protected RootObject createRootObject() { return new RootObject(); }
これにより、派生クラスでcreateRootObjectメソッドをオーバーライドして派生したRootObjectのインスタンスをOreProgに利用させることができるようになります。
3点目は、単純に解答するのであれば、RootObject#functionsコレクションに派生クラスから関数名を追加できるように修正すれば良いでしょう。この方法は、派生クラスからHandlerクラスを追加できるようにしてあるOreProg#putExpressionHandlerメソッドなどで利用しています。同様に
protected static class RootObject { ... protected static void addFunction(String name) { functions.add(name); } ... }
と実装することで、追加した関数をOreProg.SourceHandler#startElementの
if (RootObject.isFunction(qName)) { h = new RootHandler(qName);
の箇所で見つけることができるようになります。
しかし、この方法は実装は簡単ですが本質的な問題があります。それは、RootObject派生クラスへのメソッドはそれが目的なので忘れることはあり得ないとしても、addFunctionメソッド(上で追加したメソッド)の呼び出しを忘れる可能性があるということです。また、この手のうっかりミスは、関数の追加という実装自体は正しくできていること、xmlの記述自体にスペルミスがあり得ることから原因を別の箇所に想定しやすいことなど、複数の要因からはまりやすい=わかりにくいバグとなります。
したがって、より良い解決方法は関数名をメソッドの実装とは別に登録するのではなく、メソッドの実装そのものから関数名を判定するようにSourceHandler#startElementを修正することです。
具体的には、リフレクションのClass#getMethodsを利用して直接メソッドを探索します。SourceHandler#startElementの時点ではまだ引数についての知識がないため、Class#getMethodを利用することはできません。
この場合、あらたな修正も必要になります。それは、OreProg.SourceHandlerはRootObject派生クラスの存在を知らないということが理由です。そのため、
Methods[] ms = RootObject.class.getMethods();
とは書けません。
しかも、SourceHandlerを派生によって置き換えることもできません。なぜならOreProgで利用しているすべてのHandlerクラスはOreProg#readメソッドで利用している無名内部クラスを含めてsourceHandlerから派生しているからです。また、SourceHandlerはstatic内部クラスなので継承可能なOreProgクラス自身を参照できないため、上で導入したcreateRootObjectファクトリメソッドを呼び出すこともできません。もっとも、createRootObjecct().getClass()という呼び出しは無意味なインスタンスの生成を伴うのでいずれにしろ望ましくはありません。
ちなみに、リフレクションを利用する場合の1番簡単な修正方法は、RootObjectクラスのstaticフィールドを利用する方法でしょう。RootObjectクラスの派生クラスはメソッド名を登録するかわりに1度だけ、Classを登録するようにします。
protected static class RootObject { static Class klass = RootObject.class; protected static register(Class cls) { klass = cls; } static Class getRootObjectClass() { return klass; } ...
これによりSourceHandlerの呼び出しは次のように変わります。
Methods[] ms = RootObject.getRootObjectClass().getMethods();
なお、リフレクションを利用することで存在を確認したメソッド名についてはキャッシュするようにして、動的にRootObject#functionsコレクションが作成されるようにするなどの修正をしても良いかも知れません。
参考資料
- J2SE API ドキュメント
- The Java Programming Language Second Edition ISBN 0-201-31006-6
Initializer」という呼称で統一されています。配列の初期化を
int[] array = { 1, 2, 3, 4 };
{ 1, 2, 3, 4 }という記述も「Initializer」です。
(プログラム内ではargsという名前でアクセスできる)
* @return 終了時の実行コンテキスト
*/