001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.commons.jexl2;
018
019 import java.util.ArrayList;
020 import org.apache.commons.jexl2.parser.JexlNode;
021 import org.apache.commons.jexl2.parser.StringParser;
022
023 /**
024 * An evaluator similar to the Unified EL evaluator used in JSP/JSF based on JEXL.
025 * It is intended to be used in configuration modules, XML based frameworks or JSP taglibs
026 * and facilitate the implementation of expression evaluation.
027 * <p>
028 * An expression can mix immediate, deferred and nested sub-expressions as well as string constants;
029 * <ul>
030 * <li>The "immediate" syntax is of the form <code>"...${jexl-expr}..."</code></li>
031 * <li>The "deferred" syntax is of the form <code>"...#{jexl-expr}..."</code></li>
032 * <li>The "nested" syntax is of the form <code>"...#{...${jexl-expr0}...}..."</code></li>
033 * <li>The "composite" syntax is of the form <code>"...${jexl-expr0}... #{jexl-expr1}..."</code></li>
034 * </ul>
035 * </p>
036 * <p>
037 * Deferred & immediate expression carry different intentions:
038 * <ul>
039 * <li>An immediate expression indicate that evaluation is intended to be performed close to
040 * the definition/parsing point.</li>
041 * <li>A deferred expression indicate that evaluation is intended to occur at a later stage.</li>
042 * </ul>
043 * </p>
044 * <p>
045 * For instance: <code>"Hello ${name}, now is #{time}"</code> is a composite "deferred" expression since one
046 * of its subexpressions is deferred. Furthermore, this (composite) expression intent is
047 * to perform two evaluations; one close to its definition and another one in a later
048 * phase.
049 * </p>
050 * <p>
051 * The API reflects this feature in 2 methods, prepare and evaluate. The prepare method
052 * will evaluate the immediate subexpression and return an expression that contains only
053 * the deferred subexpressions (& constants), a prepared expression. Such a prepared expression
054 * is suitable for a later phase evaluation that may occur with a different JexlContext.
055 * Note that it is valid to call evaluate without prepare in which case the same JexlContext
056 * is used for the 2 evaluation phases.
057 * </p>
058 * <p>
059 * In the most common use-case where deferred expressions are to be kept around as properties of objects,
060 * one should parse & prepare an expression before storing it and evaluate it each time
061 * the property storing it is accessed.
062 * </p>
063 * <p>
064 * Note that nested expression use the JEXL syntax as in:
065 * <code>"#{${bar}+'.charAt(2)'}"</code>
066 * The most common mistake leading to an invalid expression being the following:
067 * <code>"#{${bar}charAt(2)}"</code>
068 * </p>
069 * <p>Also note that methods that parse evaluate expressions may throw <em>unchecked</em> ecxeptions;
070 * The {@link UnifiedJEXL.Exception} are thrown when the engine instance is in "non-silent" mode
071 * but since these are RuntimeException, user-code <em>should</em> catch them where appropriate.
072 * </p>
073 * @since 2.0
074 */
075 public final class UnifiedJEXL {
076 /** The JEXL engine instance. */
077 private final JexlEngine jexl;
078 /** The expression cache. */
079 private final JexlEngine.SoftCache<String,Expression> cache;
080 /** The default cache size. */
081 private static final int CACHE_SIZE = 256;
082 /**
083 * Creates a new instance of UnifiedJEXL with a default size cache.
084 * @param aJexl the JexlEngine to use.
085 */
086 public UnifiedJEXL(JexlEngine aJexl) {
087 this(aJexl, CACHE_SIZE);
088 }
089
090 /**
091 * Creates a new instance of UnifiedJEXL creating a local cache.
092 * @param aJexl the JexlEngine to use.
093 * @param cacheSize the number of expressions in this cache
094 */
095 public UnifiedJEXL(JexlEngine aJexl, int cacheSize) {
096 this.jexl = aJexl;
097 this.cache = aJexl.new SoftCache<String,Expression>(cacheSize);
098 }
099
100 /**
101 * Types of expressions.
102 * Each instance carries a counter index per (composite sub-) expression type.
103 * @see ExpressionBuilder
104 */
105 private static enum ExpressionType {
106 /** Constant expression, count index 0. */
107 CONSTANT(0),
108 /** Immediate expression, count index 1. */
109 IMMEDIATE(1),
110 /** Deferred expression, count index 2. */
111 DEFERRED(2),
112 /** Nested (which are deferred) expressions, count index 2. */
113 NESTED(2),
114 /** Composite expressions are not counted, index -1. */
115 COMPOSITE(-1);
116 /** The index in arrays of expression counters for composite expressions. */
117 private final int index;
118 /**
119 * Creates an ExpressionType.
120 * @param idx the index for this type in counters arrays.
121 */
122 ExpressionType(int idx) {
123 this.index = idx;
124 }
125 }
126
127 /**
128 * A helper class to build expressions.
129 * Keeps count of sub-expressions by type.
130 */
131 private static class ExpressionBuilder {
132 /** Per expression type counters. */
133 private final int[] counts;
134 /** The list of expressions. */
135 private final ArrayList<Expression> expressions;
136
137 /**
138 * Creates a builder.
139 * @param size the initial expression array size
140 */
141 ExpressionBuilder(int size) {
142 counts = new int[]{0, 0, 0};
143 expressions = new ArrayList<Expression>(size <= 0 ? 3 : size);
144 }
145
146 /**
147 * Adds an expression to the list of expressions, maintain per-type counts.
148 * @param expr the expression to add
149 */
150 void add(Expression expr) {
151 counts[expr.getType().index] += 1;
152 expressions.add(expr);
153 }
154
155 /**
156 * Builds an expression from a source, performs checks.
157 * @param el the unified el instance
158 * @param source the source expression
159 * @return an expression
160 */
161 Expression build(UnifiedJEXL el, Expression source) {
162 int sum = 0;
163 for (int count : counts) {
164 sum += count;
165 }
166 if (expressions.size() != sum) {
167 StringBuilder error = new StringBuilder("parsing algorithm error, exprs: ");
168 error.append(expressions.size());
169 error.append(", constant:");
170 error.append(counts[ExpressionType.CONSTANT.index]);
171 error.append(", immediate:");
172 error.append(counts[ExpressionType.IMMEDIATE.index]);
173 error.append(", deferred:");
174 error.append(counts[ExpressionType.DEFERRED.index]);
175 throw new IllegalStateException(error.toString());
176 }
177 // if only one sub-expr, no need to create a composite
178 if (expressions.size() == 1) {
179 return expressions.get(0);
180 } else {
181 return el.new CompositeExpression(counts, expressions, source);
182 }
183 }
184 }
185
186 /**
187 * Gets the JexlEngine underlying the UnifiedJEXL.
188 * @return the JexlEngine
189 */
190 public JexlEngine getEngine() {
191 return jexl;
192 }
193
194 /**
195 * The sole type of (runtime) exception the UnifiedJEXL can throw.
196 */
197 public static class Exception extends RuntimeException {
198 /** Serial version UID. */
199 private static final long serialVersionUID = -8201402995815975726L;
200 /**
201 * Creates a UnifiedJEXL.Exception.
202 * @param msg the exception message
203 * @param cause the exception cause
204 */
205 public Exception(String msg, Throwable cause) {
206 super(msg, cause);
207 }
208 }
209
210 /**
211 * The abstract base class for all expressions, immediate '${...}' and deferred '#{...}'.
212 */
213 public abstract class Expression {
214 /** The source of this expression (see {@link UnifiedJEXL.Expression#prepare}). */
215 protected final Expression source;
216 /**
217 * Creates an expression.
218 * @param src the source expression if any
219 */
220 Expression(Expression src) {
221 this.source = src != null ? src : this;
222 }
223
224 /**
225 * Formats this expression, adding its source string representation in
226 * comments if available: 'expression /*= source *\/'' .
227 * @return the formatted expression string
228 */
229 @Override
230 public String toString() {
231 StringBuilder strb = new StringBuilder();
232 if (source != this) {
233 strb.append(source.toString());
234 strb.append(" /*= ");
235 }
236 asString(strb);
237 if (source != this) {
238 strb.append(" */");
239 }
240 return strb.toString();
241 }
242
243 /**
244 * Generates this expression's string representation.
245 * @return the string representation
246 */
247 public String asString() {
248 StringBuilder strb = new StringBuilder();
249 asString(strb);
250 return strb.toString();
251 }
252
253 /**
254 * Adds this expression's string representation to a StringBuilder.
255 * @param strb the builder to fill
256 */
257 abstract void asString(StringBuilder strb);
258
259 /**
260 * When the expression is dependant upon immediate and deferred sub-expressions,
261 * evaluates the immediate sub-expressions with the context passed as parameter
262 * and returns this expression deferred form.
263 * <p>
264 * In effect, this binds the result of the immediate sub-expressions evaluation in the
265 * context, allowing to differ evaluation of the remaining (deferred) expression within another context.
266 * This only has an effect to nested & composite expressions that contain differed & immediate sub-expressions.
267 * </p>
268 * <p>
269 * If the underlying JEXL engine is silent, errors will be logged through its logger as warning.
270 * </p>
271 * @param context the context to use for immediate expression evaluations
272 * @return an expression or null if an error occurs and the {@link JexlEngine} is silent
273 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
274 */
275 public abstract Expression prepare(JexlContext context);
276
277 /**
278 * Evaluates this expression.
279 * <p>
280 * If the underlying JEXL engine is silent, errors will be logged through its logger as warning.
281 * </p>
282 * @param context the variable context
283 * @return the result of this expression evaluation or null if an error occurs and the {@link JexlEngine} is
284 * silent
285 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
286 */
287 public abstract Object evaluate(JexlContext context);
288
289 /**
290 * Checks whether this expression is immediate.
291 * @return true if immediate, false otherwise
292 */
293 public boolean isImmediate() {
294 return true;
295 }
296
297 /**
298 * Checks whether this expression is deferred.
299 * @return true if deferred, false otherwise
300 */
301 public final boolean isDeferred() {
302 return !isImmediate();
303 }
304
305 /**
306 * Retrieves this expression's source expression.
307 * If this expression was prepared, this allows to retrieve the
308 * original expression that lead to it.
309 * Other expressions return themselves.
310 * @return the source expression
311 */
312 public final Expression getSource() {
313 return source;
314 }
315
316 /**
317 * Gets this expression type.
318 * @return its type
319 */
320 abstract ExpressionType getType();
321
322 /**
323 * Prepares a sub-expression for interpretation.
324 * @param interpreter a JEXL interpreter
325 * @return a prepared expression
326 * @throws JexlException (only for nested & composite)
327 */
328 abstract Expression prepare(Interpreter interpreter);
329
330 /**
331 * Intreprets a sub-expression.
332 * @param interpreter a JEXL interpreter
333 * @return the result of interpretation
334 * @throws JexlException (only for nested & composite)
335 */
336 abstract Object evaluate(Interpreter interpreter);
337 }
338
339
340 /** A constant expression. */
341 private class ConstantExpression extends Expression {
342 /** The constant held by this expression. */
343 private final Object value;
344 /**
345 * Creates a constant expression.
346 * <p>
347 * If the wrapped constant is a string, it is treated
348 * as a JEXL strings with respect to escaping.
349 * </p>
350 * @param val the constant value
351 * @param source the source expression if any
352 */
353 ConstantExpression(Object val, Expression source) {
354 super(source);
355 if (val == null) {
356 throw new NullPointerException("constant can not be null");
357 }
358 if (val instanceof String) {
359 val = StringParser.buildString((String) val, false);
360 }
361 this.value = val;
362 }
363
364 /** {@inheritDoc} */
365 @Override
366 public String asString() {
367 StringBuilder strb = new StringBuilder();
368 strb.append('"');
369 asString(strb);
370 strb.append('"');
371 return strb.toString();
372 }
373
374 /** {@inheritDoc} */
375 @Override
376 ExpressionType getType() {
377 return ExpressionType.CONSTANT;
378 }
379
380 /** {@inheritDoc} */
381 @Override
382 void asString(StringBuilder strb) {
383 String str = value.toString();
384 if (value instanceof String || value instanceof CharSequence) {
385 for (int i = 0, size = str.length(); i < size; ++i) {
386 char c = str.charAt(i);
387 if (c == '"' || c == '\\') {
388 strb.append('\\');
389 }
390 strb.append(c);
391 }
392 } else {
393 strb.append(str);
394 }
395 }
396
397 /** {@inheritDoc} */
398 @Override
399 public Expression prepare(JexlContext context) {
400 return this;
401 }
402
403 /** {@inheritDoc} */
404 @Override
405 Expression prepare(Interpreter interpreter) {
406 return this;
407 }
408
409 /** {@inheritDoc} */
410 @Override
411 public Object evaluate(JexlContext context) {
412 return value;
413 }
414
415 /** {@inheritDoc} */
416 @Override
417 Object evaluate(Interpreter interpreter) {
418 return value;
419 }
420 }
421
422
423 /** The base for Jexl based expressions. */
424 private abstract class JexlBasedExpression extends Expression {
425 /** The JEXL string for this expression. */
426 protected final CharSequence expr;
427 /** The JEXL node for this expression. */
428 protected final JexlNode node;
429 /**
430 * Creates a JEXL interpretable expression.
431 * @param theExpr the expression as a string
432 * @param theNode the expression as an AST
433 * @param theSource the source expression if any
434 */
435 protected JexlBasedExpression(CharSequence theExpr, JexlNode theNode, Expression theSource) {
436 super(theSource);
437 this.expr = theExpr;
438 this.node = theNode;
439 }
440
441 /** {@inheritDoc} */
442 @Override
443 public String toString() {
444 StringBuilder strb = new StringBuilder(expr.length() + 3);
445 if (source != this) {
446 strb.append(source.toString());
447 strb.append(" /*= ");
448 }
449 strb.append(isImmediate() ? '$' : '#');
450 strb.append("{");
451 strb.append(expr);
452 strb.append("}");
453 if (source != this) {
454 strb.append(" */");
455 }
456 return strb.toString();
457 }
458
459 /** {@inheritDoc} */
460 @Override
461 public void asString(StringBuilder strb) {
462 strb.append(isImmediate() ? '$' : '#');
463 strb.append("{");
464 strb.append(expr);
465 strb.append("}");
466 }
467
468 /** {@inheritDoc} */
469 @Override
470 public Expression prepare(JexlContext context) {
471 return this;
472 }
473
474 /** {@inheritDoc} */
475 @Override
476 Expression prepare(Interpreter interpreter) {
477 return this;
478 }
479
480 /** {@inheritDoc} */
481 @Override
482 public Object evaluate(JexlContext context) {
483 return UnifiedJEXL.this.evaluate(context, this);
484 }
485
486 /** {@inheritDoc} */
487 @Override
488 Object evaluate(Interpreter interpreter) {
489 return interpreter.interpret(node);
490 }
491 }
492
493
494 /** An immediate expression: ${jexl}. */
495 private class ImmediateExpression extends JexlBasedExpression {
496 /**
497 * Creates an immediate expression.
498 * @param expr the expression as a string
499 * @param node the expression as an AST
500 * @param source the source expression if any
501 */
502 ImmediateExpression(CharSequence expr, JexlNode node, Expression source) {
503 super(expr, node, source);
504 }
505
506 /** {@inheritDoc} */
507 @Override
508 ExpressionType getType() {
509 return ExpressionType.IMMEDIATE;
510 }
511
512 /** {@inheritDoc} */
513 @Override
514 public boolean isImmediate() {
515 return true;
516 }
517 }
518
519 /** An immediate expression: ${jexl}. */
520 private class DeferredExpression extends JexlBasedExpression {
521 /**
522 * Creates a deferred expression.
523 * @param expr the expression as a string
524 * @param node the expression as an AST
525 * @param source the source expression if any
526 */
527 DeferredExpression(CharSequence expr, JexlNode node, Expression source) {
528 super(expr, node, source);
529 }
530
531 /** {@inheritDoc} */
532 @Override
533 ExpressionType getType() {
534 return ExpressionType.DEFERRED;
535 }
536
537 /** {@inheritDoc} */
538 @Override
539 public boolean isImmediate() {
540 return false;
541 }
542 }
543
544 /**
545 * A deferred expression that nests an immediate expression.
546 * #{...${jexl}...}
547 * Note that the deferred syntax is JEXL's, not UnifiedJEXL.
548 */
549 private class NestedExpression extends DeferredExpression {
550 /**
551 * Creates a nested expression.
552 * @param expr the expression as a string
553 * @param node the expression as an AST
554 * @param source the source expression if any
555 */
556 NestedExpression(CharSequence expr, JexlNode node, Expression source) {
557 super(expr, node, source);
558 if (this.source != this) {
559 throw new IllegalArgumentException("Nested expression can not have a source");
560 }
561 }
562
563 /** {@inheritDoc} */
564 @Override
565 ExpressionType getType() {
566 return ExpressionType.NESTED;
567 }
568
569 /** {@inheritDoc} */
570 @Override
571 public String toString() {
572 return expr.toString();
573 }
574
575 /** {@inheritDoc} */
576 @Override
577 public Expression prepare(JexlContext context) {
578 return UnifiedJEXL.this.prepare(context, this);
579 }
580
581 /** {@inheritDoc} */
582 @Override
583 public Expression prepare(Interpreter interpreter) {
584 String value = interpreter.interpret(node).toString();
585 JexlNode dnode = toNode(value, jexl.isDebug()? node.getInfo() : null);
586 return new DeferredExpression(value, dnode, this);
587 }
588
589 /** {@inheritDoc} */
590 @Override
591 public Object evaluate(Interpreter interpreter) {
592 return prepare(interpreter).evaluate(interpreter);
593 }
594 }
595
596
597 /** A composite expression: "... ${...} ... #{...} ...". */
598 private class CompositeExpression extends Expression {
599 /** Bit encoded (deferred count > 0) bit 1, (immediate count > 0) bit 0. */
600 private final int meta;
601 /** The list of sub-expression resulting from parsing. */
602 private final Expression[] exprs;
603 /**
604 * Creates a composite expression.
605 * @param counters counters of expression per type
606 * @param list the sub-expressions
607 * @param src the source for this expresion if any
608 */
609 CompositeExpression(int[] counters, ArrayList<Expression> list, Expression src) {
610 super(src);
611 this.exprs = list.toArray(new Expression[list.size()]);
612 this.meta = (counters[ExpressionType.DEFERRED.index] > 0 ? 2 : 0)
613 | (counters[ExpressionType.IMMEDIATE.index] > 0 ? 1 : 0);
614 }
615
616 /** {@inheritDoc} */
617 @Override
618 ExpressionType getType() {
619 return ExpressionType.COMPOSITE;
620 }
621
622 /** {@inheritDoc} */
623 @Override
624 public boolean isImmediate() {
625 // immediate if no deferred
626 return (meta & 2) == 0;
627 }
628
629 /** {@inheritDoc} */
630 @Override
631 void asString(StringBuilder strb) {
632 for (Expression e : exprs) {
633 e.asString(strb);
634 }
635 }
636
637 /** {@inheritDoc} */
638 @Override
639 public Expression prepare(JexlContext context) {
640 return UnifiedJEXL.this.prepare(context, this);
641 }
642
643 /** {@inheritDoc} */
644 @Override
645 Expression prepare(Interpreter interpreter) {
646 // if this composite is not its own source, it is already prepared
647 if (source != this) {
648 return this;
649 }
650 // we need to eval immediate expressions if there are some deferred/nested
651 // ie both immediate & deferred counts > 0, bits 1 & 0 set, (1 << 1) & 1 == 3
652 final boolean evalImmediate = meta == 3;
653 final int size = exprs.length;
654 final ExpressionBuilder builder = new ExpressionBuilder(size);
655 // tracking whether prepare will return a different expression
656 boolean eq = true;
657 for (int e = 0; e < size; ++e) {
658 Expression expr = exprs[e];
659 Expression prepared = expr.prepare(interpreter);
660 if (evalImmediate && prepared instanceof ImmediateExpression) {
661 // evaluate immediate as constant
662 Object value = prepared.evaluate(interpreter);
663 prepared = value == null ? null : new ConstantExpression(value, prepared);
664 }
665 // add it if not null
666 if (prepared != null) {
667 builder.add(prepared);
668 }
669 // keep track of expression equivalence
670 eq &= expr == prepared;
671 }
672 Expression ready = eq ? this : builder.build(UnifiedJEXL.this, this);
673 return ready;
674 }
675
676 /** {@inheritDoc} */
677 @Override
678 public Object evaluate(JexlContext context) {
679 return UnifiedJEXL.this.evaluate(context, this);
680 }
681
682 /** {@inheritDoc} */
683 @Override
684 Object evaluate(Interpreter interpreter) {
685 final int size = exprs.length;
686 Object value = null;
687 // common case: evaluate all expressions & concatenate them as a string
688 StringBuilder strb = new StringBuilder();
689 for (int e = 0; e < size; ++e) {
690 value = exprs[e].evaluate(interpreter);
691 if (value != null) {
692 strb.append(value.toString());
693 }
694 }
695 value = strb.toString();
696 return value;
697 }
698 }
699
700 /** Creates a a {@link UnifiedJEXL.Expression} from an expression string.
701 * Uses & fills up the expression cache if any.
702 * <p>
703 * If the underlying JEXL engine is silent, errors will be logged through its logger as warnings.
704 * </p>
705 * @param expression the UnifiedJEXL string expression
706 * @return the UnifiedJEXL object expression, null if silent and an error occured
707 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
708 */
709 public Expression parse(String expression) {
710 Exception xuel = null;
711 Expression stmt = null;
712 try {
713 if (cache == null) {
714 stmt = parseExpression(expression);
715 } else {
716 synchronized (cache) {
717 stmt = cache.get(expression);
718 if (stmt == null) {
719 stmt = parseExpression(expression);
720 cache.put(expression, stmt);
721 }
722 }
723 }
724 } catch (JexlException xjexl) {
725 xuel = new Exception("failed to parse '" + expression + "'", xjexl);
726 } catch (Exception xany) {
727 xuel = xany;
728 } finally {
729 if (xuel != null) {
730 if (jexl.isSilent()) {
731 jexl.logger.warn(xuel.getMessage(), xuel.getCause());
732 return null;
733 }
734 throw xuel;
735 }
736 }
737 return stmt;
738 }
739
740 /**
741 * Prepares an expression (nested & composites), handles exception reporting.
742 *
743 * @param context the JEXL context to use
744 * @param expr the expression to prepare
745 * @return a prepared expression
746 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
747 */
748 Expression prepare(JexlContext context, Expression expr) {
749 try {
750 Interpreter interpreter = jexl.createInterpreter(context);
751 interpreter.setSilent(false);
752 return expr.prepare(interpreter);
753 } catch (JexlException xjexl) {
754 Exception xuel = createException("prepare", expr, xjexl);
755 if (jexl.isSilent()) {
756 jexl.logger.warn(xuel.getMessage(), xuel.getCause());
757 return null;
758 }
759 throw xuel;
760 }
761 }
762
763 /**
764 * Evaluates an expression (nested & composites), handles exception reporting.
765 *
766 * @param context the JEXL context to use
767 * @param expr the expression to prepare
768 * @return the result of the evaluation
769 * @throws UnifiedJEXL.Exception if an error occurs and the {@link JexlEngine} is not silent
770 */
771 Object evaluate(JexlContext context, Expression expr) {
772 try {
773 Interpreter interpreter = jexl.createInterpreter(context);
774 interpreter.setSilent(false);
775 return expr.evaluate(interpreter);
776 } catch (JexlException xjexl) {
777 Exception xuel = createException("evaluate", expr, xjexl);
778 if (jexl.isSilent()) {
779 jexl.logger.warn(xuel.getMessage(), xuel.getCause());
780 return null;
781 }
782 throw xuel;
783 }
784 }
785
786 /**
787 * Use the JEXL parser to create the AST for an expression.
788 * @param expression the expression to parse
789 * @return the AST
790 * @throws JexlException if an error occur during parsing
791 */
792 private JexlNode toNode(CharSequence expression) {
793 return jexl.parse(expression, null);
794 }
795
796 /**
797 * Use the JEXL parser to create the AST for an expression.
798 * @param expression the expression to parse
799 * @param info debug information
800 * @return the AST
801 * @throws JexlException if an error occur during parsing
802 */
803 private JexlNode toNode(CharSequence expression, JexlInfo info) {
804 return jexl.parse(expression, info);
805 }
806
807 /**
808 * Creates a UnifiedJEXL.Exception from a JexlException.
809 * @param action parse, prepare, evaluate
810 * @param expr the expression
811 * @param xany the exception
812 * @return an exception containing an explicit error message
813 */
814 private Exception createException(String action, Expression expr, java.lang.Exception xany) {
815 StringBuilder strb = new StringBuilder("failed to ");
816 strb.append(action);
817 strb.append(" '");
818 strb.append(expr.toString());
819 strb.append("'");
820 Throwable cause = xany.getCause();
821 if (cause != null) {
822 String causeMsg = cause.getMessage();
823 if (causeMsg != null) {
824 strb.append(", ");
825 strb.append(causeMsg);
826 }
827 }
828 return new Exception(strb.toString(), xany);
829 }
830
831
832 /** The different parsing states. */
833 private static enum ParseState {
834 /** Parsing a constant. */
835 CONST,
836 /** Parsing after $ .*/
837 IMMEDIATE0,
838 /** Parsing after # .*/
839 DEFERRED0,
840 /** Parsing after ${ .*/
841 IMMEDIATE1,
842 /** Parsing after #{ .*/
843 DEFERRED1,
844 /** Parsing after \ .*/
845 ESCAPE
846 }
847
848 /**
849 * Parses a unified expression.
850 * @param expr the string expression
851 * @return the expression instance
852 * @throws JexlException if an error occur during parsing
853 */
854 private Expression parseExpression(String expr) {
855 final int size = expr.length();
856 ExpressionBuilder builder = new ExpressionBuilder(0);
857 StringBuilder strb = new StringBuilder(size);
858 ParseState state = ParseState.CONST;
859 int inner = 0;
860 boolean nested = false;
861 int inested = -1;
862 for (int i = 0; i < size; ++i) {
863 char c = expr.charAt(i);
864 switch (state) {
865 default: // in case we ever add new expression type
866 throw new UnsupportedOperationException("unexpected expression type");
867 case CONST:
868 if (c == '$') {
869 state = ParseState.IMMEDIATE0;
870 } else if (c == '#') {
871 inested = i;
872 state = ParseState.DEFERRED0;
873 } else if (c == '\\') {
874 state = ParseState.ESCAPE;
875 } else {
876 // do buildup expr
877 strb.append(c);
878 }
879 break;
880 case IMMEDIATE0: // $
881 if (c == '{') {
882 state = ParseState.IMMEDIATE1;
883 // if chars in buffer, create constant
884 if (strb.length() > 0) {
885 Expression cexpr = new ConstantExpression(strb.toString(), null);
886 builder.add(cexpr);
887 strb.delete(0, Integer.MAX_VALUE);
888 }
889 } else {
890 // revert to CONST
891 strb.append('$');
892 strb.append(c);
893 state = ParseState.CONST;
894 }
895 break;
896 case DEFERRED0: // #
897 if (c == '{') {
898 state = ParseState.DEFERRED1;
899 // if chars in buffer, create constant
900 if (strb.length() > 0) {
901 Expression cexpr = new ConstantExpression(strb.toString(), null);
902 builder.add(cexpr);
903 strb.delete(0, Integer.MAX_VALUE);
904 }
905 } else {
906 // revert to CONST
907 strb.append('#');
908 strb.append(c);
909 state = ParseState.CONST;
910 }
911 break;
912 case IMMEDIATE1: // ${...
913 if (c == '}') {
914 // materialize the immediate expr
915 Expression iexpr = new ImmediateExpression(strb.toString(), toNode(strb), null);
916 builder.add(iexpr);
917 strb.delete(0, Integer.MAX_VALUE);
918 state = ParseState.CONST;
919 } else {
920 // do buildup expr
921 strb.append(c);
922 }
923 break;
924 case DEFERRED1: // #{...
925 // skip inner strings (for '}')
926 if (c == '"' || c == '\'') {
927 strb.append(c);
928 i = StringParser.readString(strb, expr, i + 1, c);
929 continue;
930 }
931 // nested immediate in deferred; need to balance count of '{' & '}'
932 if (c == '{') {
933 if (expr.charAt(i - 1) == '$') {
934 inner += 1;
935 strb.deleteCharAt(strb.length() - 1);
936 nested = true;
937 }
938 continue;
939 }
940 // closing '}'
941 if (c == '}') {
942 // balance nested immediate
943 if (inner > 0) {
944 inner -= 1;
945 } else {
946 // materialize the nested/deferred expr
947 Expression dexpr = null;
948 if (nested) {
949 dexpr = new NestedExpression(expr.substring(inested, i + 1), toNode(strb), null);
950 } else {
951 dexpr = new DeferredExpression(strb.toString(), toNode(strb), null);
952 }
953 builder.add(dexpr);
954 strb.delete(0, Integer.MAX_VALUE);
955 nested = false;
956 state = ParseState.CONST;
957 }
958 } else {
959 // do buildup expr
960 strb.append(c);
961 }
962 break;
963 case ESCAPE:
964 if (c == '#') {
965 strb.append('#');
966 } else if (c == '$') {
967 strb.append('$');
968 } else {
969 strb.append('\\');
970 strb.append(c);
971 }
972 state = ParseState.CONST;
973 }
974 }
975 // we should be in that state
976 if (state != ParseState.CONST) {
977 throw new Exception("malformed expression: " + expr, null);
978 }
979 // if any chars were buffered, add them as a constant
980 if (strb.length() > 0) {
981 Expression cexpr = new ConstantExpression(strb.toString(), null);
982 builder.add(cexpr);
983 }
984 return builder.build(this, null);
985 }
986 }