MessageInterpolator.java
package com.github.mygreen.messageformatter;
import java.util.Deque;
import java.util.Formatter;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.support.MessageSourceAccessor;
import com.github.mygreen.messageformatter.expression.ExpressionEvaluator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 名前付き変数のメッセージをフォーマットするクラス。
* <p><code>{...}</code>の場合、変数を単純に置換する。</p>
* <p><code>${...}</code>の場合、EL式を利用し処理する。</p>
* <p>文字'$', '{', '}'は特殊文字のため、<code>\</code>でエスケープを行う。</p>
*
*
* @author T.TSUCHIE
*
*/
@Slf4j
@RequiredArgsConstructor
public class MessageInterpolator {
/**
* EL式を評価する処理
*/
private final ExpressionEvaluator expressionEvaluator;
/**
* メッセージを引数varsで指定した変数で補完する。
*
* @param message 対象のメッセージ。
* @param vars メッセージ中の変数に対する値のマップ。
* @return 補完したメッセージ。
*/
public String interpolate(final String message, final Map<String, ?> vars) {
return parse(message, vars, false, 0, 0, null);
}
/**
* メッセージを引数varsで指定した変数で補完する。
*
* @param message 対象のメッセージ。
* @param vars メッセージ中の変数に対する値のマップ。
* @param messageSource メッセージを解決するクラス。nullの場合、指定しないと同じ意味になります。
* @return 補完したメッセージ。
*/
public String interpolate(final String message, final Map<String, ?> vars, final MessageSourceAccessor messageSource) {
return parse(message, vars, false, 0, 0, messageSource);
}
/**
* メッセージを引数varsで指定した変数で補完する。
* <p>変換したメッセージに対しても再帰的に処理します。</p>
*
* @param message 対象のメッセージ。
* @param vars メッセージ中の変数に対する値のマップ。
* @param maxRecursion 再帰的にメッセージを処理する最大回数。0以下を指定したときは再帰回数の制限はありません。
* @return 補完したメッセージ。
*/
public String interpolate(final String message, final Map<String, ?> vars, int maxRecursion) {
return parse(message, vars, true, maxRecursion, 0, null);
}
/**
* メッセージを引数varsで指定した変数で補完する。
* <p>変換したメッセージに対しても再帰的に処理します。</p>
* <p>{@link MessageSourceAccessor}を指定した場合、メッセージ中の変数をメッセージコードとして解決します。
*
* @param message 対象のメッセージ。
* @param vars メッセージ中の変数に対する値のマップ。
* @param maxRecursion 再帰的にメッセージを処理する最大回数。0以下を指定したときは再帰回数の制限はありません。
* @param messageSource メッセージを解決するクラス。nullの場合、指定しないと同じ意味になります。
* @return 補完したメッセージ。
*/
public String interpolate(final String message, final Map<String, ?> vars, int maxRecursion,
final MessageSourceAccessor messageSource) {
return parse(message, vars, true, maxRecursion, 0, messageSource);
}
/**
* メッセージをパースし、変数に値を差し込み、EL式を評価する。
* @param message 対象のメッセージ。
* @param vars メッセージ中の変数に対する値のマップ。
* @param recursive 変換したメッセージに対しても再帰的に処理するかどうか。
* @param maxRecursion 再帰的にメッセージを処理する最大回数。0以下を指定したときは再帰回数の制限はありません。
* @param recursiveCount 現在の再帰回数
* @param messageSource メッセージを解決するクラス。nullの場合、指定しないと同じ意味になります。
* @return 補完したメッセージ。
*/
protected String parse(final String message, final Map<String, ?> vars, boolean recursive, int maxRecursion,
int recursiveCount, final MessageSourceAccessor messageSource) {
// 評価したメッセージを格納するバッファ。
final StringBuilder sb = new StringBuilder(message.length());
/*
* 変数とEL式を解析する際に使用する、スタック変数。
* 式の開始が現れたらスタックに積み、式の終了が現れたらスタックから全てを取り出す。
* スタックに積まれるのは、1つ文の変数またはEL式。
*/
final Deque<String> stack = new LinkedList<>();
final int length = message.length();
for(int i=0; i < length; i++) {
final char c = message.charAt(i);
if(StackUtils.equalsTopElement(stack, "\\")) {
// 直前の文字がエスケープ文字の場合、エスケープ文字として結合する。
String escapedChar = StackUtils.popup(stack) + c;
if(!stack.isEmpty()) {
// 取り出した後もスタックがある場合は、式の途中であるため、再度スタックに積む。
stack.push(escapedChar);
} else {
// 取り出した後にスタックがない場合は、エスケープを解除して通常の文字として積む。
sb.append(c);
}
} else if(c == '\\') {
// エスケープ文字の場合はスタックに積む。
stack.push(String.valueOf(c));
} else if(c == '$') {
stack.push(String.valueOf(c));
} else if(c == '{') {
if(!stack.isEmpty() && !StackUtils.equalsAnyBottomElement(stack, new String[]{"$", "{"})) {
// スタックの先頭が式の開始形式でない場合
throw new MessageParseException(message, "expression not start with '{' or '$'");
} else {
stack.push(String.valueOf(c));
}
} else if(c == '}') {
if(StackUtils.equalsAnyBottomElement(stack, new String[]{"{", "$"})) {
// 式の終わりの場合は、式を取り出し評価する。
String expression = StackUtils.popupAndConcat(stack) + c;
// エスケープを解除する
expression = removeEscapeChar(expression, '\\');
String result = evaluate(expression, vars, recursive, maxRecursion, recursiveCount, messageSource);
sb.append(result);
} else {
sb.append(c);
}
} else {
if(stack.isEmpty()) {
sb.append(c);
} else {
stack.push(String.valueOf(c));
}
}
}
if(!stack.isEmpty()) {
String val = StackUtils.popupAndConcat(stack);
val = removeEscapeChar(val, '\\');
sb.append(val);
}
return sb.toString();
}
private String evaluate(final String expression, final Map<String, ?> values, final boolean recursive,
final int maxRecursion, final int recursiveCount, final MessageSourceAccessor messageSource) {
if(expression.startsWith("{")) {
// 変数の置換の場合
final String varName = expression.substring(1, expression.length()-1);
if(values.containsKey(varName)) {
// 該当するキーが存在する場合
final Object value = values.get(varName);
final String eval = (value == null) ? "" : value.toString();
if(!eval.isEmpty() && recursivable(recursive, maxRecursion, recursiveCount, eval)) {
return parse(eval, values, recursive, maxRecursion, recursiveCount+1, messageSource);
} else {
return eval;
}
} else if(messageSource != null) {
// メッセージコードをとして解決をする。
String eval;
try {
eval = messageSource.getMessage(varName);
} catch(NoSuchMessageException e) {
// 該当するキーが存在しない場合は、値をそのまま返す。
return String.format("{%s}", varName);
}
if(recursivable(recursive, maxRecursion, recursiveCount, eval)) {
return parse(eval, values, recursive, maxRecursion, recursiveCount+1, messageSource);
} else {
return eval;
}
} else {
// 該当するキーが存在しない場合は、値をそのまま返す。
return String.valueOf(expression);
}
} else if(expression.startsWith("${")) {
// EL式で処理する
final String expr = expression.substring(2, expression.length()-1);
final String eval = evaluateExpression(expr, values);
if(recursivable(recursive, maxRecursion, recursiveCount, eval)) {
return parse(eval, values, recursive, maxRecursion, recursiveCount+1, messageSource);
} else {
return eval;
}
}
throw new MessageParseException(expression, "not support expression.");
}
/**
* 現在の再帰回数が最大回数に達しているかどうか。
* @param recursive 再帰的に処理するかどうか。
* @param maxRecursion 最大再帰回数
* @param currentCount 再帰回数
* @param message 再帰対象のメッセージ
* @return 最大再帰回数を超えていなければfalseを返す。
*/
private boolean recursivable(final boolean recursive, final int maxRecursion, final int currentCount,
String message) {
if(!recursive) {
return false;
}
if(maxRecursion <= 0) {
// 再帰回数の制限なし。
return true;
}
if(currentCount <= maxRecursion) {
return true;
}
log.warn("Over recursive count : currentCount={}, maxCount={}, message={}.", currentCount, maxRecursion, message);
return false;
}
/**
* EL式を評価する。
* @param expression EL式
* @param values EL式中の変数。
* @return 評価した式。
*/
protected String evaluateExpression(final String expression, final Map<String, ?> values) {
final Map<String, Object> context = new LinkedHashMap<>();
context.putAll(values);
// フォーマッターの追加
context.computeIfAbsent("formatter", key -> new Formatter());
/*
* SpELで存在しない変数名の場合、nullが帰ってくるため、null判定を行う。
*/
Object eval = expressionEvaluator.evaluate(expression, context);
String value = eval == null ? "" : eval.toString();
if(log.isTraceEnabled()) {
log.trace("evaluate expression language: expression='{}' ===> value='{}'", expression, value);
}
return value;
}
/**
* エスケープ文字を除去した文字列を取得する。
* @param str
* @param escapeChar
* @return
*/
private static String removeEscapeChar(final String str, final char escapeChar) {
if(str == null || str.isEmpty()) {
return str;
}
final String escapeStr = String.valueOf(escapeChar);
StringBuilder sb = new StringBuilder();
LinkedList<String> stack = new LinkedList<>();
final int length = str.length();
for(int i=0; i < length; i++) {
final char c = str.charAt(i);
if(StackUtils.equalsTopElement(stack, escapeStr)) {
// スタックの一番上がエスケープ文字の場合
StackUtils.popup(stack);
sb.append(c);
} else if(c == escapeChar) {
// スタックに積む
stack.push(String.valueOf(c));
} else {
sb.append(c);
}
}
if(!stack.isEmpty()) {
sb.append(StackUtils.popupAndConcat(stack));
}
return sb.toString();
}
}