KeyBreakProcessor
先日記事にしたCollectionsと似たようなものではあるが・・・
時々キーブレイク(コントロールブレイク?)の処理を書くことがある。
先日プロジェクトの新人メンバーが言うには考え方はわかるが、javaでどう書いたらいいかわからないとのこと。
言われてみれば、そこそこ使うアルゴリズムなわりにはテンプレートなものってないよな・・・
キーブレイクの処理で重要なのは、ブレイクする条件と、ブレイクした時に何をするのかの2つ。
そこだけ個別に実装すればいいように作ってみた。
最初は簡単なコードで済んでいたのだが、要素の削除や追加まで考えたらかなり複雑になってしまった。
それなりに動作確認はしているが、細かいところまではあまり自信がない・・・
使い方はjavadocコメントを見て。
/** * {@link KeyBreakProcessor}のサブクラスでオーバーライドされた{@link KeyBreakProcessor#checkBreak(Object, Object)}、{@link KeyBreakProcessor#onBeforeBreak(String, Object, Object)}、{@link KeyBreakProcessor#onItem(Object)}、{@link KeyBreakProcessor#onAfterBreak(String, Object, Object)}からthrowされたExceptionを内包するクラス。 */ public class KeyBreakException extends Exception { private Object current; /** * * @param current エラー発生時に処理対象だったコレクションの要素 * @param exception 発生したエラー */ public KeyBreakException(Object current, Exception exception) { super(exception); this.current = current; } /** * エラー発生時に処理対象だったコレクションの要素を返す。 * @return */ public Object getCurrent() {return current;} }
import java.text.MessageFormat; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Stack; /** * コレクションのキーブレイク処理のテンプレートクラス。<br/> * <br/> * {@link #checkBreak(Object, Object) checkBreak(V, V)} でキーブレイクの条件を定義し、{@link #onBeforeBreak(String, Object, Object) onBeforeBreak(String, V, V)}、{@link #onAfterBreak(String, Object, Object) onAfterBreak(String, V, V)}にそれぞれキーブレイクした際の処理を記述する。<br/> * コレクション内の個々の要素に対して行う処理は{@link #onItem(Object) onItem(V)}に記述する。<br/> * <br/> * コレクションは事前にキーの昇順または降順でソートされている必要がある。<br/> * <br/> * キーごとのカウントを出力する例:<br/> * <pre> * List<String[]> list = new ArrayList<String[]>(); * list.add(new String[] {"aaa", "111", "a1"}); * list.add(new String[] {"aaa", "222", "a2"}); * list.add(new String[] {"aaa", "222", "a2"}); * list.add(new String[] {"aaa", "333", "a3"}); * list.add(new String[] {"aaa", "333", "a3"}); * list.add(new String[] {"aaa", "333", "a3"}); * list.add(new String[] {"bbb", "111", "b1"}); * list.add(new String[] {"bbb", "222", "b2"}); * list.add(new String[] {"bbb", "222", "b2"}); * list.add(new String[] {"bbb", "333", "b3"}); * list.add(new String[] {"bbb", "333", "b3"}); * list.add(new String[] {"bbb", "333", "b3"}); * KeyBreakProcessor<String[]> processor = new KeyBreakProcessor<String[]>(new String[] {"key1", "key2"}) { // 2つのキーを使用 * public String checkBreak(String[] current, String[] previous) { * // 最初の要素が異なればkey1のブレイクとする * if (!previous[0].equals(current[0])) { * return "key1"; * } * // 2番目の要素が異なればkey2のブレイクとする * if (!previous[1].equals(current[1])) { * return "key2"; * } * // それ以外はブレイクしない * return NoBreak; * } * public void onAfterBreak(String keyName, String[] current, String[] next) { * // ブレイクしたキーごとにキーの値とカウントを出力する * if (keyName.equals("key1")) { * System.out.println("key = " + current[0] + ", count = " + ((int[])getContext(keyName).get("count"))[0]); * } else { * System.out.println("key = " + current[0] + ":" + current[1] + ", count = " + ((int[])getContext(keyName).get("count"))[0]); * } * } * public void onBeforeBreak(String keyName, String[] current, String[] previous) { * // キーごとのカウントを初期化する * getContext(keyName).put("count", new int[1]); * } * public void onItem(String[] current) { * // キーごとのカウントを増やす * ((int[]) getContext("key1").get("count"))[0]++; * ((int[]) getContext("key2").get("count"))[0]++; * } * }; * processor.process(list); * </pre> * 出力結果:<br/> * <pre> * key = aaa:111, count = 1 * key = aaa:222, count = 2 * key = aaa:333, count = 3 * key = aaa, count = 6 * key = bbb:111, count = 1 * key = bbb:222, count = 2 * key = bbb:333, count = 3 * key = bbb, count = 6 * </pre><br/> * キーごとのカウントを集計行としてリストに追加する例:<br/> * <br/> * 上記の例のonAfterBreak()を下記のように修正する。<br/> * <pre> * public void onAfterBreak(String keyName, String[] current, String[] next) { * // ブレイクしたキーごとに集計行を追加する * if (keyName.equals("key1")) { * addNext(new String[] {current[0], "", String.valueOf(((int[])getContext(keyName).get("count"))[0])}); * } else { * addNext(new String[] {"", current[1], String.valueOf(((int[])getContext(keyName).get("count"))[0])}); * } * } * </pre> * process()実行後にリストの内容を出力する。<br/> * <pre> * for (int i = 0; i < list.size(); i++) { * System.out.println(Arrays.asList(list.get(i)).toString()); * } * </pre> * 出力結果:<br/> * <pre> * [aaa, 111, a1] * [, 111, 1] * [aaa, 222, a2] * [aaa, 222, a2] * [, 222, 2] * [aaa, 333, a3] * [aaa, 333, a3] * [aaa, 333, a3] * [, 333, 3] * [aaa, , 6] * [bbb, 111, b1] * [, 111, 1] * [bbb, 222, b2] * [bbb, 222, b2] * [, 222, 2] * [bbb, 333, b3] * [bbb, 333, b3] * [bbb, 333, b3] * [, 333, 3] * [bbb, , 6] * </pre><br/> * キーが複数ある場合、優先順位の高いキーのブレイクはそれより低い優先順位のキーのブレイクも伴う。<br/> * <br/> * 上記の例では、{@link #checkBreak(Object, Object) checkBreak(V, V)}で、current = {"bbb", "111", "b1"}、previous = {"aaa", "333", "a3"}の時に"key1"を戻り値に指定しており、以下の順番でメソッドが呼び出される。<br/> * 1.onAfterBreak("key2", current({"aaa", "333", "a3"}), next({"bbb", "111", "b1"}))<br/> * 2.onAfterBreak("key1", current({"aaa", "333", "a3"}), next({"bbb", "111", "b1"}))<br/> * 3.onBeforeBreak("key1", current({"bbb", "111", "b1"}), previous({"aaa", "333", "a3"}))<br/> * 4.onBeforeBreak("key2", current({"bbb", "111", "b1"}), previous({"aaa", "333", "a3"}))<br/> */ public abstract class KeyBreakProcessor<V> { private static int indexOf(String[] array, String item) { for (int i = 0; i < array.length; i++) { if (array[i].equals(item)) { return i; } } return -1; } // キーの配列 private String[] keyNames; // リスト private List<V> list; // リストのインデックス private int currentIndex; // コレクションの列挙子 private Iterator<V> iterator; /** * キーブレイクしないことを示す値。<br/> * コンストラクタでkeyNamesに含まれない値に初期化される。 */ protected final String NoBreak; // キーごとのコンテキスト private Map<String, Map<Object, Object>> context = new HashMap<String, Map<Object, Object>>(); // addPrevious()、addNext()で追加された要素の数。checkBreak()から後、onAfterBreak()まではその要素に対するイベントの発生終了が確定しないのでスタックで最新の要素に対するカウントを保持する private Stack<int[]> addPreviousCount = new Stack<int[]>(); private Stack<int[]> addNextCount = new Stack<int[]>(); // 要素が削除されたか否かのフラグ private boolean removed; // コレクションのループ処理の終了フラグ private int stopFlag; /** * ブレイクするキーの配列を指定する。<br/> * nullと空文字以外の任意の文字列。<br/> * 先頭の要素から優先順位が高いキーとして扱う。<br/> * @param keyNames * @throws NullPointerException keyNamesがnullの場合。keyNamesの要素がnullの場合。 * @throws IllegalArgumentException keyNamesが空の配列の場合。keyNamesの要素が空文字の場合。keyNamesに重複する要素がある場合。 */ public KeyBreakProcessor(String[] keyNames) { if (keyNames == null) { throw new NullPointerException("keyNames must not null"); } if (keyNames.length == 0) { throw new IllegalArgumentException("keyNames is empty"); } for (int i = 0; i < keyNames.length; i++) { if (keyNames[i] == null) { throw new NullPointerException(MessageFormat.format("keyNames[{0}] must not null", new Object[] {new Integer(i)})); } if (keyNames[i].length() == 0) { throw new IllegalArgumentException(MessageFormat.format("keyNames[{0}] is empty", new Object[] {new Integer(i)})); } // 重複要素のチェック for (int j = i + 1; j < keyNames.length; j++) { if (keyNames[i].equals(keyNames[j])) { throw new IllegalArgumentException(MessageFormat.format("duplicate key keyNames[{0}] and keyNames[{1}]", new Object[] {new Integer(i), new Integer(j)})); } } // キーごとのコンテキストの初期化 context.put(keyNames[i], new HashMap<Object, Object>()); } this.keyNames = keyNames; // キーブレイクしないことを示すキー文字列の作成 // keyNamesに存在しない文字列とする String noBreak = null; boolean existKey = false; do { noBreak = toString() + "#NoBreak#" + Math.ceil(Math.random() * Integer.MAX_VALUE); existKey = indexOf(keyNames, noBreak) >= 0; } while (existKey); NoBreak = noBreak; } /** * コレクションに次の要素があればtrueを返す。 * @return */ private boolean hasNext() { if (iterator == null) { return currentIndex + (addNextCount.isEmpty() ? 0 : ((int[]) addNextCount.peek())[0]) < list.size(); } else { return iterator.hasNext(); } } /** * コレクションの次の要素を返す。 * @return * @throws NoSuchElementException 繰り返し処理でそれ以上要素がない場合 */ private V next() { if (iterator == null) { if (!hasNext()) { throw new NoSuchElementException(); } return list.get(currentIndex++); } else { return iterator.next(); } } /** * iteratorが返す要素に対して処理を行う。<br/> * {@link #addPrevious(Object) addPrevious(V)}、{@link #addNext(Object) addNext(V)}、{@link #remove()}でのコレクションの変更は出来ない。<br/> * 対象のコレクションが大きく、Listとして保持できない場合に使用する。 * @param iterator * @throws KeyBreakException * @throws NullPointerException iteratorがnullの場合 */ public void process(Iterator<V> iterator) throws KeyBreakException { if (iterator == null) { throw new NullPointerException("iterator must not null"); } this.iterator = iterator; list = null; process(); } /** * listに対して処理を行う。<br/> * {@link #addPrevious(Object) addPrevious(V)}、{@link #addNext(Object) addNext(V)}、{@link #remove()}でのコレクションの変更が出来る。 * @param list * @throws KeyBreakException * @throws NullPointerException listがnullの場合 */ public void process(List<V> list) throws KeyBreakException { if (list == null) { throw new NullPointerException("list must not null"); } this.list = list; iterator = null; currentIndex = 0; addPreviousCount.clear(); addNextCount.clear(); removed = false; process(); } /** * コレクションの処理を開始。 * @param collection * @throws KeyBreakException */ private void process() throws KeyBreakException { // 直前の要素 V previous = null; // 現在の要素 V current = null; // 先頭の要素を処理したか否かのフラグ boolean first = true; // previousに対応するインデックス int previousIndex = -1; // afterBreakTargetの初期値。コレクションの要素にはnullが有り得るため、nullは初期値としては使えない。 final Object dummy = new Object(); // onAfterBreak()を呼び出す対象の要素を保持する。 // ほとんどの場合previousと同じ要素を示すがクリアするタイミングが異なる。 // previous = 単純に直前に処理した要素を保持。クリアするタイミングはonAfterBreak()呼出し後にremovedフラグが有る場合のみ。(=その要素が削除されたことを示す) // afterBreakTarget = onAfterBreak()を呼び出すときのcurrentとして使用する。クリアするタイミングはonAfterBreak()呼出し後。(=previousに対応するキーのonAfterBreak()が済んでいることを示す) Object afterBreakTarget = dummy; // 最後にブレイクしたキーのインデックス int lastBreakKey = -1; stopFlag = 0; // すべての要素を処理 while (hasNext()) { // addNext()で追加された要素は無視する if (list != null) currentIndex += addNextCount.isEmpty() ? 0 : ((int[]) addNextCount.peek())[0]; // 前の要素が削除されていたら追加カウンタを削除する if (removed) { addPreviousCount.pop(); addNextCount.pop(); } current = next(); // 新しい要素に対する追加カウンタを追加 addPreviousCount.push(new int[1]); addNextCount.push(new int[1]); removed = false; String key = null; // 2番目以降の要素の場合、キーブレイクが必要かどうかチェックする if (!first) { try { key = checkBreak(current, previous); } catch (Exception e) { throw new KeyBreakException(current, e); } // stop()が呼ばれたらループを終了する if (stopFlag != 0) { if (stopFlag == 2) { // 最後の要素に対するonAfterBreak()をキャンセルする afterBreakTarget = dummy; } break; } // 削除されたら以降の処理はスキップ if (removed) { continue; } } int breakIndex = -1; if (first) { // 最初の要素は先頭からすべてのキーをブレイク breakIndex = 0; } else if (!NoBreak.equals(key)) { // キーブレイク指定されたら、そのインデックスを調べる breakIndex = indexOf(keyNames, key); // キーがコンストラクタに指定されたものでなければエラー if (breakIndex == -1) { throw new IllegalStateException(MessageFormat.format("unknown key:{0}", new Object[] {key})); } // onAfterBreak()直後の場合、その時のキーに対しては必ずonBeforeBreak()を呼び出す必要があるので、インデックスの小さい方とする if (afterBreakTarget == dummy) { breakIndex = Math.min(breakIndex, lastBreakKey); } } else if (afterBreakTarget == dummy) { // ブレイクなしの場合も、onAfterBreak()直後はその時のキーでonBeforeBreak()を呼び出す breakIndex = lastBreakKey; } // onAfterBreak()処理 if (afterBreakTarget != dummy && key != null && !key.equals(NoBreak)) { // 最後のキーから指定されたキーまでブレイクの後始末をする // onAfterBreak()の場合、previousが示す要素を現在の処理対象として渡すので、一時的にインデックスをpreviousの時の値に変更する int[][] oldCount = new int[2][]; // iterator使用の場合はコレクションの変更が無いのでインデックスの調整は不要 if (list != null) { currentIndex = previousIndex; // previousの追加カウンタを最新にする oldCount[0] = (int[]) addPreviousCount.pop(); oldCount[1] = (int[]) addNextCount.pop(); } for (; lastBreakKey >= breakIndex && !removed && stopFlag == 0; lastBreakKey--) { try { onAfterBreak(keyNames[lastBreakKey], previous, current); // ブレイクしたキーのコンテキスト情報をクリアする context.get(keyNames[lastBreakKey]).clear(); } catch (Exception e) { throw new KeyBreakException(previous, e); } } // インデックスを元に戻す if (list != null) { // onAfterBreak()でどう追加削除されようが、previousに対するcurrentのインデックスはaddCount+1+(currentのaddPrevious) currentIndex += ((int[]) addNextCount.peek())[0] + 1 + oldCount[0][0]; // もうprevious以前の要素に対してイベントが発生することは無いので古いキーに対応する追加カウンタを削除 addPreviousCount.clear(); addNextCount.clear(); // currentの追加カウンタを戻す addPreviousCount.push(oldCount[0]); addNextCount.push(oldCount[1]); } // removed()が呼ばれていたらpreviousをクリアする if (removed) { previous = null; previousIndex = -1; } // onAfterBreak()を呼び出したらループ終了後に再度呼び出さないように変数をリセットする afterBreakTarget = dummy; removed = false; // stop()が呼ばれたらループを終了する if (stopFlag != 0) { break; } } if (breakIndex != -1) { // 指定されたキーから最後のキーまでブレイクの前処理をする for (int i = breakIndex; i < keyNames.length && !removed && stopFlag == 0; i++) { try { // ブレイクするキーのコンテキスト情報をクリアする context.get(keyNames[i]).clear(); onBeforeBreak(keyNames[i], current, previous); lastBreakKey = i; } catch (Exception e) { throw new KeyBreakException(current, e); } } // stop()が呼ばれたらループを終了する if (stopFlag != 0) { if (stopFlag == 2) { // 最後の要素に対するonAfterBreak()をキャンセルする afterBreakTarget = dummy; } else if (!removed) { // 削除されてなければ今の要素をonAfterBreak()の対象にする afterBreakTarget = current; } break; } // 削除されたら以降の処理はスキップ if (removed) { continue; } } // レコードごとの処理を呼び出す try { onItem(current); } catch (Exception e) { throw new KeyBreakException(current, e); } // stop(true)が呼ばれたらループを終了する if (stopFlag == 2) { // 最後の要素に対するonAfterBreak()をキャンセルする afterBreakTarget = dummy; break; } // 削除されたら以降の処理はスキップ if (removed) { continue; } // 処理した要素を次の要素との比較のため保持する previous = current; // addPreviousで要素を追加されると直前の要素のインデックスがわからなくなるので変数で保持 previousIndex = currentIndex; current = null; first = false; // onAfterBreak()を呼び出す対象として保持 afterBreakTarget = previous; // stop()が呼ばれたらループを終了する if (stopFlag != 0) { break; } } // 最後の要素で、最後にすべてのキーに対してブレイクの後始末をする if (afterBreakTarget != dummy) { currentIndex = previousIndex; // 前の要素が削除されていたら追加カウンタを削除する if (removed) { addPreviousCount.pop(); addNextCount.pop(); } removed = false; stopFlag = 0; for (; lastBreakKey >= 0 && !removed && stopFlag == 0; lastBreakKey--) { try { onAfterBreak(keyNames[lastBreakKey], (V) afterBreakTarget, null); } catch (Exception e) { throw new KeyBreakException(afterBreakTarget, e); } } } // すべてのキーのコンテキスト情報をクリアする for (int i = 0; i < keyNames.length; i++) { context.get(keyNames[i]).clear(); } } /** * その時点でのキーごとのコンテキスト情報を返す。<br/> * このクラスを利用する側で、キーごとに状態を保持するためのMapを提供する。<br/> * このクラスはMapの内容には関知しないが、ブレイクするたびに内容をクリアする。<br/> * @param key * @return */ protected Map<Object, Object> getContext(String key) { return context.get(key); } /** * 現在処理している要素の前に任意の要素を追加する。<br/> * remove()で対象の要素を削除した場合でも、その要素があったインデックスを基準に追加する。<br/> * iterator使用時は使用できない。<br/> * 追加した要素を削除することはできない。<br/> * 追加した要素に対しては各onXXXメソッドは呼び出されない。 * @param object * @throws UnsupportedOperationException iterator使用時に呼び出した場合。 */ protected void addPrevious(V object) { if (iterator != null) { throw new UnsupportedOperationException(); } if (removed) { list.add(currentIndex++, object); } else { list.add(currentIndex++ - 1, object); } ((int[])addPreviousCount.peek())[0]++; } /** * 現在処理している要素の次に任意の要素を追加する。 * remove()で対象の要素を削除した場合でも、その要素があったインデックスを基準に追加する。<br/> * 同じ要素に対して複数回呼び出した場合、追加される位置は前回の呼び出しで追加した要素の次となる。<br/> * iterator使用時は使用できない。<br/> * 追加した要素を削除することはできない。<br/> * 追加した要素に対しては各onXXXメソッドは呼び出されない。 * @param object * @throws UnsupportedOperationException iterator使用時に呼び出した場合。 */ protected void addNext(V object) { if (iterator != null) { throw new UnsupportedOperationException(); } list.add(currentIndex + addNextCount.peek()[0]++, object); } /** * 現在処理している要素を削除する。<br/> * 同じ要素に対して複数回の削除はできない。<br/> * iterator使用時は使用できない。<br/> * @throws UnsupportedOperationException iterator使用時に呼び出した場合 * @throws IllegalStateException すでに削除した要素に対して呼び出した場合 */ protected void remove() { if (iterator != null) { throw new UnsupportedOperationException(); } if (removed) { throw new IllegalStateException(); } list.remove(--currentIndex); removed = true; } /** * コレクションの処理を終了する。 * @param immediate trueにすると処理中のキーに対する{@link #onAfterBreak(String, Object, Object) onAfterBreak(String, V, V)}は呼び出されない。 */ protected void stop(boolean immediate) { if (stopFlag != 0) { throw new IllegalStateException(); } stopFlag = immediate ? 2 : 1; } /** * キーブレイクの直前に呼び出される。<br/> * このメソッド内で{@link #remove()}を呼び出すと、currentに対しては{@link #onItem(Object) onItem(V)}は呼び出されず、次の要素では必ず{@link #onBeforeBreak(String, Object, Object) onBeforeBreak(String, V, V)}が呼び出される。 * @param keyName ブレイクしたキーの名前 * @param current ブレイクした要素 * @param previous 前の要素。その要素が削除された場合はnull。 * @throws Exception */ protected abstract void onBeforeBreak(String keyName, V current, V previous) throws Exception; /** * キーブレイクの直後に呼び出される。<br/> * このメソッド内で{@link #remove()}を呼び出すと、直後の{@link #onBeforeBreak(String, Object, Object) onBeforeBreak(String, V, V)}のpreviousにはnullが渡される。 * @param keyName ブレイクしたキーの名前 * @param current ブレイクした要素 * @param next 次の要素 * @throws Exception */ protected abstract void onAfterBreak(String keyName, V current, V next) throws Exception; /** * 個々の要素に対して呼び出される。<br/> * このメソッド内で{@link #remove()}を呼び出すと、その要素に対しては{@link #onAfterBreak(String, Object, Object) onAfterBreak(String, V, V)}は呼び出されない。<br/> * その要素がキーの最後の要素だった場合、同じキーに該当し、削除されていない前の要素に対して{@link #onAfterBreak(String, Object, Object) onAfterBreak(String, V, V)}が呼び出される。<br/> * キーに含まれる要素がその1件しかなければ{@link #onAfterBreak(String, Object, Object) onAfterBreak(String, V, V)}は呼び出されない。<br/> * @param current * @throws Exception */ protected abstract void onItem(V current) throws Exception; /** * コレクションの要素ごとに、前の要素との比較を行ってブレイクするキーを判断する。<br/> * キーが複数有る場合、必ず優先順位の高いキーからブレイクの判断をすること。<br/> * 先頭の要素に対しては呼び出されない。<br/> * {@link #NoBreak}メンバを返すとブレイクしない。<br/> * このメソッド内で{@link #remove()}を呼び出すと、次の要素に対して再度呼び出される。<br/> * {@link #remove()}を呼び出した場合、戻り値は無視される。<br/> * @param current 処理対象の要素 * @param previous 前の要素 * @return ブレイクするキーの名前。ブレイクしない場合は{@link #NoBreak}メンバ変数 * @throws Exception */ protected abstract String checkBreak(V current, V previous) throws Exception; }