View Javadoc
1   package com.github.mygreen.supercsv.localization;
2   
3   import java.util.Formatter;
4   import java.util.HashMap;
5   import java.util.LinkedHashMap;
6   import java.util.LinkedList;
7   import java.util.Map;
8   import java.util.Objects;
9   import java.util.Optional;
10  
11  import org.slf4j.Logger;
12  import org.slf4j.LoggerFactory;
13  
14  import com.github.mygreen.supercsv.expression.CustomFunctions;
15  import com.github.mygreen.supercsv.expression.ExpressionEvaluationException;
16  import com.github.mygreen.supercsv.expression.ExpressionLanguage;
17  import com.github.mygreen.supercsv.expression.ExpressionLanguageJEXLImpl;
18  import com.github.mygreen.supercsv.util.StackUtils;
19  
20  /**
21   * 名前付き変数のメッセージをフォーマットするクラス。
22   * <p><code>{...}</code>の場合、変数を単純に置換する。
23   * <p><code>${...}</code>の場合、EL式を利用し処理する。
24   * <p>文字'$', '{', '}'は特殊文字のため、<code>\</code>でエスケープを行う。
25   * <p>ELのパーサは、{@link ExpressionLanguage}の実装クラスで切り替え可能。
26   * <p>{@link MessageResolver}を指定した場合、メッセージ中の変数<code>{...}</code>をメッセージ定義コードとして解決する。
27   *    ただし、メッセージ変数で指定されている変数が優先される。
28   * 
29   * @since 2.0
30   * @author T.TSUCHIE
31   *
32   */
33  public class MessageInterpolator {
34      
35      private static final Logger logger = LoggerFactory.getLogger(MessageInterpolator.class);
36      
37      private ExpressionLanguage expressionLanguage;
38      
39      /**
40       * デフォルトのコンストラクタ
41       * <p>式言語の処理実装として、JEXLの{@link ExpressionLanguageJEXLImpl} が設定されます。
42       *   <br>さらに、関数として{@link CustomFunctions}が登録されており、接頭語 {@literal f:}で呼び出し可能です。
43       * </p>
44       * 
45       */
46      public MessageInterpolator() {
47          
48          // EL式中で使用可能な関数の登録
49          ExpressionLanguageJEXLImplpressionLanguageJEXLImpl.html#ExpressionLanguageJEXLImpl">ExpressionLanguageJEXLImpl el = new ExpressionLanguageJEXLImpl();
50          
51          Map<String, Object> funcs = new HashMap<>(); 
52          funcs.put("f", CustomFunctions.class);
53          el.getJexlEngine().setFunctions(funcs);
54          
55          setExpressionLanguage(el);
56      }
57      
58      /**
59       * 式言語の実装を指定するコンストラクタ
60       * @param expressionLanguage EL式を評価する実装。
61       */
62      public MessageInterpolator(final ExpressionLanguage expressionLanguage) {
63          Objects.requireNonNull(expressionLanguage, "expressionLanguage should not be null.");
64          this.expressionLanguage = expressionLanguage;
65      }
66      
67      /**
68       * メッセージを引数varsで指定した変数で補完する。
69       * 
70       * @param message 対象のメッセージ。
71       * @param vars メッセージ中の変数に対する値のマップ。
72       * @return 補完したメッセージ。
73       */
74      public String interpolate(final String message, final Map<String, ?> vars) {
75          return interpolate(message, vars, false);
76      }
77      
78      /**
79       * メッセージを引数varsで指定した変数で補完する。
80       * 
81       * @param message 対象のメッセージ。
82       * @param vars メッセージ中の変数に対する値のマップ。
83       * @param recursive 変換したメッセージに対しても再帰的に処理するかどうか
84       * @return 補完したメッセージ。
85       */
86      public String interpolate(final String message, final Map<String, ?> vars, boolean recursive) {
87          return parse(message, vars, recursive, null);
88      }
89      
90      /**
91       * メッセージを引数varsで指定した変数で補完する。
92       * <p>{@link MessageResolver}を指定した場合、メッセージ中の変数をメッセージコードとして解決します。
93       * 
94       * @param message 対象のメッセージ。
95       * @param vars メッセージ中の変数に対する値のマップ。
96       * @param recursive 変換したメッセージに対しても再帰的に処理するかどうか
97       * @param messageResolver メッセージを解決するクラス。nullの場合、指定しないと同じ意味になります。
98       * @return 補完したメッセージ。
99       */
100     public String interpolate(final String message, final Map<String, ?> vars, boolean recursive,
101             final MessageResolver messageResolver) {
102         return parse(message, vars, recursive, messageResolver);
103     }
104     
105     /**
106      * メッセージをパースし、変数に値を差し込み、EL式を評価する。
107      * @param message 対象のメッセージ。
108      * @param vars メッセージ中の変数に対する値のマップ。
109      * @param messageResolver メッセージを解決するクラス。nullの場合、指定しないと同じ意味になります。
110      * @return 補完したメッセージ。
111      */
112     protected String parse(final String message, final Map<String, ?> vars, boolean recursive, final MessageResolver messageResolver) {
113         
114         // 評価したメッセージを格納するバッファ。
115         final StringBuilder sb = new StringBuilder(message.length());
116         
117         /*
118          * 変数とEL式を解析する際に使用する、スタック変数。
119          * 式の開始が現れたらスタックに積み、式の終了が現れたらスタックから全てを取り出す。
120          * スタックに積まれるのは、1つ文の変数またはEL式。
121          */
122         final LinkedList<String> stack = new LinkedList<String>();
123         
124         final int length = message.length();
125         
126         for(int i=0; i < length; i++) {
127             final char c = message.charAt(i);
128             
129             if(StackUtils.equalsTopElement(stack, "\\")) {
130                 // 直前の文字がエスケープ文字の場合、エスケープ文字として結合する。
131                 String escapedChar = StackUtils.popup(stack) + c;
132                 
133                 if(!stack.isEmpty()) {
134                     // 取り出した後もスタックがある場合は、式の途中であるため、再度スタックに積む。
135                     stack.push(escapedChar);
136                     
137                 } else {
138                     // 取り出した後にスタックがない場合は、エスケープを解除して通常の文字として積む。
139                     sb.append(c);
140                     
141                 }
142                 
143             } else if(c == '\\') {
144                 // エスケープ文字の場合はスタックに積む。
145                 stack.push(String.valueOf(c));
146                 
147             } else if(c == '$') {
148                 stack.push(String.valueOf(c));
149                 
150             } else if(c == '{') {
151                 
152                 if(!stack.isEmpty() && !StackUtils.equalsAnyBottomElement(stack, new String[]{"$", "{"})) {
153                     // スタックの先頭が式の開始形式でない場合
154                     throw new MessageParseException(message, "expression not start with '{' or '$'");
155                     
156                 } else {
157                     stack.push(String.valueOf(c));
158                 }
159                 
160                 
161             } else if(c == '}') {
162                 
163                 if(StackUtils.equalsAnyBottomElement(stack, new String[]{"{", "$"})) {
164                     // 式の終わりの場合は、式を取り出し評価する。
165                     String expression = StackUtils.popupAndConcat(stack) + c;
166                     
167                     // エスケープを解除する
168                     expression = removeEscapeChar(expression, '\\');
169                     
170                     String result = evaluate(expression, vars, recursive, messageResolver);
171                     sb.append(result);
172                     
173                 } else {
174                     sb.append(c);
175                     
176                 }
177                 
178             } else {
179                 
180                 if(stack.isEmpty()) {
181                     sb.append(c);
182                     
183                 } else {
184                     stack.push(String.valueOf(c));
185                 }
186                 
187             }
188             
189         }
190         
191         if(!stack.isEmpty()) {
192             String val = StackUtils.popupAndConcat(stack);
193             val = removeEscapeChar(val, '\\');
194             sb.append(val);
195         }
196         
197         return sb.toString();
198     }
199     
200     private String evaluate(final String expression, final Map<String, ?> values, final boolean recursive,
201             final MessageResolver messageResolver) {
202         
203         if(expression.startsWith("{")) {
204             // 変数の置換の場合
205             final String varName = expression.substring(1, expression.length()-1);
206             
207             if(values.containsKey(varName)) {
208                 // 該当するキーが存在する場合
209                 final Object value = values.get(varName);
210                 final String eval = (value == null) ? "" : value.toString();
211                 if(!eval.isEmpty() && recursive) {
212                     return parse(eval, values, recursive, messageResolver);
213                 } else {
214                     return eval;
215                 }
216                 
217             } else if(messageResolver != null) {
218                 // メッセージコードをとして解決をする。
219                 final Optional<String> eval = messageResolver.getMessage(varName);
220                 if(!eval.isPresent()) {
221                     // 該当するキーが存在しない場合は、値をそのまま返す。
222                     return String.format("{%s}", varName);
223                 }
224                 
225                 if(recursive) {
226                     return parse(eval.get(), values, recursive, messageResolver);
227                 } else {
228                     return eval.get();
229                 }
230                 
231             } else {
232                 // 該当するキーが存在しない場合は、値をそのまま返す。
233                 return expression.toString();
234             }
235             
236         } else if(expression.startsWith("${")) {
237             // EL式で処理する
238             final String expr = expression.substring(2, expression.length()-1);
239             final String eval = evaluateExpression(expr, values);
240             if(recursive) {
241                 return parse(eval, values, recursive, messageResolver);
242             } else {
243                 return eval;
244             }
245             
246         }
247         
248         throw new MessageParseException(expression, "not support expression.");
249         
250     }
251     
252     /**
253      * EL式を評価する。
254      * @param expression EL式
255      * @param values EL式中の変数。
256      * @return 評価した式。
257      * @throws ExpressionEvaluationException 
258      */
259     protected String evaluateExpression(final String expression, final Map<String, ?> values) throws ExpressionEvaluationException {
260         
261         final Map<String, Object> context = new LinkedHashMap<String, Object>();
262         context.putAll(values);
263         
264         // フォーマッターの追加
265         context.computeIfAbsent("formatter", key -> new Formatter());
266         
267         final String value = expressionLanguage.evaluate(expression, context).toString();
268         if(logger.isTraceEnabled()) {
269             logger.trace("evaluate expression language: expression='{}' ===> value='{}'", expression, value);
270         }
271         
272         return value;
273     }
274     
275     /**
276      * エスケープ文字を除去した文字列を取得する。
277      * @param str
278      * @param escapeChar
279      * @return
280      */
281     private String removeEscapeChar(final String str, final char escapeChar) {
282         
283         if(str == null || str.isEmpty()) {
284             return str;
285         }
286         
287         final String escapeStr = String.valueOf(escapeChar);
288         StringBuilder sb = new StringBuilder();
289         
290         final LinkedList<String> stack = new LinkedList<>();
291         
292         final int length = str.length();
293         for(int i=0; i < length; i++) {
294             final char c = str.charAt(i);
295             
296             if(StackUtils.equalsTopElement(stack, escapeStr)) {
297                 // スタックの一番上がエスケープ文字の場合
298                 StackUtils.popup(stack);
299                 sb.append(c);
300                 
301             } else if(c == escapeChar) {
302                 // スタックに積む
303                 stack.push(String.valueOf(c));
304                 
305             } else {
306                 sb.append(c);
307             }
308             
309         }
310         
311         if(!stack.isEmpty()) {
312             sb.append(StackUtils.popupAndConcat(stack));
313         }
314         
315         return sb.toString();
316         
317     }
318     
319     /**
320      * EL式を解析する実装クラスを取得する。
321      * @return
322      */
323     public ExpressionLanguage getExpressionLanguage() {
324         return expressionLanguage;
325     }
326     
327     /**
328      * EL式を解析する実装クラスを設定する。
329      * @param expressionLanguage EL式の解析するクラスの実装。
330      */
331     public void setExpressionLanguage(ExpressionLanguage expressionLanguage) {
332         this.expressionLanguage = expressionLanguage;
333     }
334     
335 }