MessageInterpolator.java

package com.github.mygreen.supercsv.localization;

import java.util.Formatter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.mygreen.supercsv.expression.CustomFunctions;
import com.github.mygreen.supercsv.expression.ExpressionEvaluationException;
import com.github.mygreen.supercsv.expression.ExpressionLanguage;
import com.github.mygreen.supercsv.expression.ExpressionLanguageJEXLImpl;
import com.github.mygreen.supercsv.util.StackUtils;

/**
 * 名前付き変数のメッセージをフォーマットするクラス。
 * <p><code>{...}</code>の場合、変数を単純に置換する。
 * <p><code>${...}</code>の場合、EL式を利用し処理する。
 * <p>文字'$', '{', '}'は特殊文字のため、<code>\</code>でエスケープを行う。
 * <p>ELのパーサは、{@link ExpressionLanguage}の実装クラスで切り替え可能。
 * <p>{@link MessageResolver}を指定した場合、メッセージ中の変数<code>{...}</code>をメッセージ定義コードとして解決する。
 *    ただし、メッセージ変数で指定されている変数が優先される。
 * 
 * @since 2.0
 * @author T.TSUCHIE
 *
 */
public class MessageInterpolator {
    
    private static final Logger logger = LoggerFactory.getLogger(MessageInterpolator.class);
    
    private ExpressionLanguage expressionLanguage;
    
    /**
     * デフォルトのコンストラクタ
     * <p>式言語の処理実装として、JEXLの{@link ExpressionLanguageJEXLImpl} が設定されます。
     *   <br>さらに、関数として{@link CustomFunctions}が登録されており、接頭語 {@literal f:}で呼び出し可能です。
     * </p>
     * 
     */
    public MessageInterpolator() {
        
        // EL式中で使用可能な関数の登録
        ExpressionLanguageJEXLImpl el = new ExpressionLanguageJEXLImpl();
        
        Map<String, Object> funcs = new HashMap<>(); 
        funcs.put("f", CustomFunctions.class);
        el.getJexlEngine().setFunctions(funcs);
        
        setExpressionLanguage(el);
    }
    
    /**
     * 式言語の実装を指定するコンストラクタ
     * @param expressionLanguage EL式を評価する実装。
     */
    public MessageInterpolator(final ExpressionLanguage expressionLanguage) {
        Objects.requireNonNull(expressionLanguage, "expressionLanguage should not be null.");
        this.expressionLanguage = expressionLanguage;
    }
    
    /**
     * メッセージを引数varsで指定した変数で補完する。
     * 
     * @param message 対象のメッセージ。
     * @param vars メッセージ中の変数に対する値のマップ。
     * @return 補完したメッセージ。
     */
    public String interpolate(final String message, final Map<String, ?> vars) {
        return interpolate(message, vars, false);
    }
    
    /**
     * メッセージを引数varsで指定した変数で補完する。
     * 
     * @param message 対象のメッセージ。
     * @param vars メッセージ中の変数に対する値のマップ。
     * @param recursive 変換したメッセージに対しても再帰的に処理するかどうか
     * @return 補完したメッセージ。
     */
    public String interpolate(final String message, final Map<String, ?> vars, boolean recursive) {
        return parse(message, vars, recursive, null);
    }
    
    /**
     * メッセージを引数varsで指定した変数で補完する。
     * <p>{@link MessageResolver}を指定した場合、メッセージ中の変数をメッセージコードとして解決します。
     * 
     * @param message 対象のメッセージ。
     * @param vars メッセージ中の変数に対する値のマップ。
     * @param recursive 変換したメッセージに対しても再帰的に処理するかどうか
     * @param messageResolver メッセージを解決するクラス。nullの場合、指定しないと同じ意味になります。
     * @return 補完したメッセージ。
     */
    public String interpolate(final String message, final Map<String, ?> vars, boolean recursive,
            final MessageResolver messageResolver) {
        return parse(message, vars, recursive, messageResolver);
    }
    
    /**
     * メッセージをパースし、変数に値を差し込み、EL式を評価する。
     * @param message 対象のメッセージ。
     * @param vars メッセージ中の変数に対する値のマップ。
     * @param messageResolver メッセージを解決するクラス。nullの場合、指定しないと同じ意味になります。
     * @return 補完したメッセージ。
     */
    protected String parse(final String message, final Map<String, ?> vars, boolean recursive, final MessageResolver messageResolver) {
        
        // 評価したメッセージを格納するバッファ。
        final StringBuilder sb = new StringBuilder(message.length());
        
        /*
         * 変数とEL式を解析する際に使用する、スタック変数。
         * 式の開始が現れたらスタックに積み、式の終了が現れたらスタックから全てを取り出す。
         * スタックに積まれるのは、1つ文の変数またはEL式。
         */
        final LinkedList<String> stack = new LinkedList<String>();
        
        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, messageResolver);
                    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 MessageResolver messageResolver) {
        
        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() && recursive) {
                    return parse(eval, values, recursive, messageResolver);
                } else {
                    return eval;
                }
                
            } else if(messageResolver != null) {
                // メッセージコードをとして解決をする。
                final Optional<String> eval = messageResolver.getMessage(varName);
                if(!eval.isPresent()) {
                    // 該当するキーが存在しない場合は、値をそのまま返す。
                    return String.format("{%s}", varName);
                }
                
                if(recursive) {
                    return parse(eval.get(), values, recursive, messageResolver);
                } else {
                    return eval.get();
                }
                
            } else {
                // 該当するキーが存在しない場合は、値をそのまま返す。
                return expression.toString();
            }
            
        } else if(expression.startsWith("${")) {
            // EL式で処理する
            final String expr = expression.substring(2, expression.length()-1);
            final String eval = evaluateExpression(expr, values);
            if(recursive) {
                return parse(eval, values, recursive, messageResolver);
            } else {
                return eval;
            }
            
        }
        
        throw new MessageParseException(expression, "not support expression.");
        
    }
    
    /**
     * EL式を評価する。
     * @param expression EL式
     * @param values EL式中の変数。
     * @return 評価した式。
     * @throws ExpressionEvaluationException 
     */
    protected String evaluateExpression(final String expression, final Map<String, ?> values) throws ExpressionEvaluationException {
        
        final Map<String, Object> context = new LinkedHashMap<String, Object>();
        context.putAll(values);
        
        // フォーマッターの追加
        context.computeIfAbsent("formatter", key -> new Formatter());
        
        final String value = expressionLanguage.evaluate(expression, context).toString();
        if(logger.isTraceEnabled()) {
            logger.trace("evaluate expression language: expression='{}' ===> value='{}'", expression, value);
        }
        
        return value;
    }
    
    /**
     * エスケープ文字を除去した文字列を取得する。
     * @param str
     * @param escapeChar
     * @return
     */
    private 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();
        
        final 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();
        
    }
    
    /**
     * EL式を解析する実装クラスを取得する。
     * @return
     */
    public ExpressionLanguage getExpressionLanguage() {
        return expressionLanguage;
    }
    
    /**
     * EL式を解析する実装クラスを設定する。
     * @param expressionLanguage EL式の解析するクラスの実装。
     */
    public void setExpressionLanguage(ExpressionLanguage expressionLanguage) {
        this.expressionLanguage = expressionLanguage;
    }
    
}