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