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