FM牛鍵屋本舗

プログラマ(弱)の日々精進系ブログ

# Javaで簡易テンプレートを置換するよ!!

Javaで簡易テンプレートを置換するよ!!

仕事でテンプレートのプレースホルダーっぽいのを置換することになりました。

~テンプレートエンジン使えよ~

プレースホルダーはある特定の文字列から始まり、特定の文字列で終わります。

置換する文字列は元を辿れば画面入力、なので上記の特定の文字列が入力されても置換されないようにする必要があります。

そのため、単純なreplaceではだめで、どうしようかなーと考えた結果、一旦プレースホルダーを残したままsplitで配列化したあとに、ループ処理で置換することにしました。

~最初の仕様では置換箇所一つだったから問題なかったのに…~

さあどうやってsplitしてやろうか!!

apache-commonsのStringUtils

困ったときのStringUtils。

JavaDocを舐めてみましたが、置換文字列を残すのは難しそう。

java.util.StringTokenizer

コンストラクタだけで仕様満たせるとか神ってる…!!と思ったら

StringTokenizerは、互換性を維持する目的で保持されているレガシー・クラスであり、新規コードでは使用が推奨されていません。この機能の使用を考えているなら、Stringのsplitメソッドまたはjava.util.regexパッケージを代わりに使用することをお薦めします

Oops.

java.lang.String

ずばりそのものはない。

正規表現で後読みとか駆使すれば出来なくはない(と思う)けど、これはまたの機会に。

力技

もともとテンプレートエンジン使えよってところからスタートしているので、どう転んでも車輪の再発明

それなら力技でやってやる…!!

インターフェース

すでにプレースホルダーが複数Enumに定義されていたのでインターフェース作成。

後述の通り、こいつが開始文字列と終了文字列を持っていてもいいのかもしれない。

package sample.template;

public interface ReplacementHolder {
     String getReplacement();
}

置換文字列の実態を保持しているEnum

package sample.template;

public enum PlaceholderName implements ReplacementHolder {
    /** 猫の名前 */
    CAT_NAME("#{catName}#")
    /** 犬の名前 */
    , DOG_NAME("#{dogName}#")
    ;
    /** コンストラクタ */
    PlaceholderName(String replacement) {
        this.replacement = replacement;
    }
    private String replacement;
    public String getReplacement() {
        return this.replacement;
    }
}

分割した各トークンを表すクラス

package sample.template;

public class Token {

    /** トークン */
    private String token;

    /** 置換対象か */
    private boolean replaces;

    /** コンストラクタ */
    public Token(String token) {
        this(token, false);
    }

    /** コンストラクタ */
    public Token(String token, boolean replaces) {
        this.token = token;
        this.replaces = replaces;
    }

    /** 更新 */
    public void update(String token) {
        if (this.replaces && token != null) {
            this.token =  token;
        }
        this.replaces = false;
    }
    public boolean replaces() {
        return this.replaces;
    }
    public boolean notReplaces() {
        return !this.replaces;
    }
    public String getToken() {
        return this.token;
    }
}

実際に処理するクラス

package sample.template;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Replacer<T extends Enum<?> & ReplacementHolder >{

    /** 開始マーカー */
    private String startMarker;
    /** 終了マーカー */
    private String endMarker;

    /** コンストラクタ BeanValidatorとかで契約的にしたほうがよいかも */
    public Replacer(String startMaker, String endMarker) {
        if (startMaker == null || endMarker == null) {
            throw new IllegalArgumentException("不正な引数");
        }
        this.startMarker = startMaker;
        this.endMarker = endMarker;
    }

    /** 置換 */
    public String replace(String template, Map<T, String> replacements) {
        // 前提条件チェックコードは省略
        List<Token> tokens = this.tokenize(template);
        for (Token token : tokens) {
            if (token.notReplaces()) {
                continue;
            }
            replacements.keySet().stream()
                    .filter(key -> key.getReplacement().equals(token.getToken()))
                    .findFirst() // 同じキーが二度指定されている場合は事前にチェック処理を入れるしかない
                    .ifPresent(key -> token.update(replacements.get(key)));
        }
        return this.serialize(tokens);
    }
    /** 直列化 */
    public String serialize(List<Token> tokens) {
        // 前提条件チェックコードは省略
        return tokens.stream().filter(Token::notReplaces).map(Token::getToken).collect(Collectors.joining());
    }
    /** トークン化 */
    public List<Token> tokenize(String src) {
        List<Token> tokens = new ArrayList<>();
        if (src == null) {
            return tokens;
        }
        int start;
        while(0 <= (start = src.indexOf(startMarker))) { // 開始マーカーが存在する間ループ
            /* 終了位置を取得 */
            int end = src.indexOf(endMarker, start);
            if (end < 0) {
                end = src.length();
            } else {
                end += endMarker.length();
            }
            if (start != 0) { // 先頭に開始文字列がある場合は分割しない
                tokens.add(new Token(src.substring(0, start)));
            }
            final String parts = src.substring(start, end);
            tokens.add(new Token(parts, parts.endsWith(endMarker)));
            src = src.substring(end);
        }
        if (src != "") {
            tokens.add(new Token(src));
        }
        return tokens;
    }
}

試す

package sample.template;

import java.util.LinkedHashMap;
import java.util.Map;

public class ReplacerTest {

    public static void main(String[] args) {
        Replacer<PlaceholderName> replacer = new Replacer<>("#{", "}#"); // ジェネリクスから取得できるようにしてもいいかも
        Map<PlaceholderName, String> map = new LinkedHashMap<>();
        map.put(PlaceholderName.CAT_NAME, "小太郎");
        map.put(PlaceholderName.DOG_NAME, "シロ");

        System.out.println(replacer.replace("#{catName}#ちゃんと#{dogName}#ちゃんは仲良しです#{hoge", map));
    }

}

小太郎ちゃんとシロちゃんは仲良しです#{hoge

うーん、あんまりスマートじゃないなあ。