001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.expression.common;
018
019import java.util.ArrayDeque;
020import java.util.ArrayList;
021import java.util.Deque;
022import java.util.List;
023
024import org.springframework.expression.Expression;
025import org.springframework.expression.ExpressionParser;
026import org.springframework.expression.ParseException;
027import org.springframework.expression.ParserContext;
028import org.springframework.lang.Nullable;
029
030/**
031 * An expression parser that understands templates. It can be subclassed by expression
032 * parsers that do not offer first class support for templating.
033 *
034 * @author Keith Donald
035 * @author Juergen Hoeller
036 * @author Andy Clement
037 * @since 3.0
038 */
039public abstract class TemplateAwareExpressionParser implements ExpressionParser {
040
041        @Override
042        public Expression parseExpression(String expressionString) throws ParseException {
043                return parseExpression(expressionString, null);
044        }
045
046        @Override
047        public Expression parseExpression(String expressionString, @Nullable ParserContext context) throws ParseException {
048                if (context != null && context.isTemplate()) {
049                        return parseTemplate(expressionString, context);
050                }
051                else {
052                        return doParseExpression(expressionString, context);
053                }
054        }
055
056
057        private Expression parseTemplate(String expressionString, ParserContext context) throws ParseException {
058                if (expressionString.isEmpty()) {
059                        return new LiteralExpression("");
060                }
061
062                Expression[] expressions = parseExpressions(expressionString, context);
063                if (expressions.length == 1) {
064                        return expressions[0];
065                }
066                else {
067                        return new CompositeStringExpression(expressionString, expressions);
068                }
069        }
070
071        /**
072         * Helper that parses given expression string using the configured parser. The
073         * expression string can contain any number of expressions all contained in "${...}"
074         * markers. For instance: "foo${expr0}bar${expr1}". The static pieces of text will
075         * also be returned as Expressions that just return that static piece of text. As a
076         * result, evaluating all returned expressions and concatenating the results produces
077         * the complete evaluated string. Unwrapping is only done of the outermost delimiters
078         * found, so the string 'hello ${foo${abc}}' would break into the pieces 'hello ' and
079         * 'foo${abc}'. This means that expression languages that used ${..} as part of their
080         * functionality are supported without any problem. The parsing is aware of the
081         * structure of an embedded expression. It assumes that parentheses '(', square
082         * brackets '[' and curly brackets '}' must be in pairs within the expression unless
083         * they are within a string literal and a string literal starts and terminates with a
084         * single quote '.
085         * @param expressionString the expression string
086         * @return the parsed expressions
087         * @throws ParseException when the expressions cannot be parsed
088         */
089        private Expression[] parseExpressions(String expressionString, ParserContext context) throws ParseException {
090                List<Expression> expressions = new ArrayList<>();
091                String prefix = context.getExpressionPrefix();
092                String suffix = context.getExpressionSuffix();
093                int startIdx = 0;
094
095                while (startIdx < expressionString.length()) {
096                        int prefixIndex = expressionString.indexOf(prefix, startIdx);
097                        if (prefixIndex >= startIdx) {
098                                // an inner expression was found - this is a composite
099                                if (prefixIndex > startIdx) {
100                                        expressions.add(new LiteralExpression(expressionString.substring(startIdx, prefixIndex)));
101                                }
102                                int afterPrefixIndex = prefixIndex + prefix.length();
103                                int suffixIndex = skipToCorrectEndSuffix(suffix, expressionString, afterPrefixIndex);
104                                if (suffixIndex == -1) {
105                                        throw new ParseException(expressionString, prefixIndex,
106                                                        "No ending suffix '" + suffix + "' for expression starting at character " +
107                                                        prefixIndex + ": " + expressionString.substring(prefixIndex));
108                                }
109                                if (suffixIndex == afterPrefixIndex) {
110                                        throw new ParseException(expressionString, prefixIndex,
111                                                        "No expression defined within delimiter '" + prefix + suffix +
112                                                        "' at character " + prefixIndex);
113                                }
114                                String expr = expressionString.substring(prefixIndex + prefix.length(), suffixIndex);
115                                expr = expr.trim();
116                                if (expr.isEmpty()) {
117                                        throw new ParseException(expressionString, prefixIndex,
118                                                        "No expression defined within delimiter '" + prefix + suffix +
119                                                        "' at character " + prefixIndex);
120                                }
121                                expressions.add(doParseExpression(expr, context));
122                                startIdx = suffixIndex + suffix.length();
123                        }
124                        else {
125                                // no more ${expressions} found in string, add rest as static text
126                                expressions.add(new LiteralExpression(expressionString.substring(startIdx)));
127                                startIdx = expressionString.length();
128                        }
129                }
130
131                return expressions.toArray(new Expression[0]);
132        }
133
134        /**
135         * Return true if the specified suffix can be found at the supplied position in the
136         * supplied expression string.
137         * @param expressionString the expression string which may contain the suffix
138         * @param pos the start position at which to check for the suffix
139         * @param suffix the suffix string
140         */
141        private boolean isSuffixHere(String expressionString, int pos, String suffix) {
142                int suffixPosition = 0;
143                for (int i = 0; i < suffix.length() && pos < expressionString.length(); i++) {
144                        if (expressionString.charAt(pos++) != suffix.charAt(suffixPosition++)) {
145                                return false;
146                        }
147                }
148                if (suffixPosition != suffix.length()) {
149                        // the expressionString ran out before the suffix could entirely be found
150                        return false;
151                }
152                return true;
153        }
154
155        /**
156         * Copes with nesting, for example '${...${...}}' where the correct end for the first
157         * ${ is the final }.
158         * @param suffix the suffix
159         * @param expressionString the expression string
160         * @param afterPrefixIndex the most recently found prefix location for which the
161         * matching end suffix is being sought
162         * @return the position of the correct matching nextSuffix or -1 if none can be found
163         */
164        private int skipToCorrectEndSuffix(String suffix, String expressionString, int afterPrefixIndex)
165                        throws ParseException {
166
167                // Chew on the expression text - relying on the rules:
168                // brackets must be in pairs: () [] {}
169                // string literals are "..." or '...' and these may contain unmatched brackets
170                int pos = afterPrefixIndex;
171                int maxlen = expressionString.length();
172                int nextSuffix = expressionString.indexOf(suffix, afterPrefixIndex);
173                if (nextSuffix == -1) {
174                        return -1; // the suffix is missing
175                }
176                Deque<Bracket> stack = new ArrayDeque<>();
177                while (pos < maxlen) {
178                        if (isSuffixHere(expressionString, pos, suffix) && stack.isEmpty()) {
179                                break;
180                        }
181                        char ch = expressionString.charAt(pos);
182                        switch (ch) {
183                                case '{':
184                                case '[':
185                                case '(':
186                                        stack.push(new Bracket(ch, pos));
187                                        break;
188                                case '}':
189                                case ']':
190                                case ')':
191                                        if (stack.isEmpty()) {
192                                                throw new ParseException(expressionString, pos, "Found closing '" + ch +
193                                                                "' at position " + pos + " without an opening '" +
194                                                                Bracket.theOpenBracketFor(ch) + "'");
195                                        }
196                                        Bracket p = stack.pop();
197                                        if (!p.compatibleWithCloseBracket(ch)) {
198                                                throw new ParseException(expressionString, pos, "Found closing '" + ch +
199                                                                "' at position " + pos + " but most recent opening is '" + p.bracket +
200                                                                "' at position " + p.pos);
201                                        }
202                                        break;
203                                case '\'':
204                                case '"':
205                                        // jump to the end of the literal
206                                        int endLiteral = expressionString.indexOf(ch, pos + 1);
207                                        if (endLiteral == -1) {
208                                                throw new ParseException(expressionString, pos,
209                                                                "Found non terminating string literal starting at position " + pos);
210                                        }
211                                        pos = endLiteral;
212                                        break;
213                        }
214                        pos++;
215                }
216                if (!stack.isEmpty()) {
217                        Bracket p = stack.pop();
218                        throw new ParseException(expressionString, p.pos, "Missing closing '" +
219                                        Bracket.theCloseBracketFor(p.bracket) + "' for '" + p.bracket + "' at position " + p.pos);
220                }
221                if (!isSuffixHere(expressionString, pos, suffix)) {
222                        return -1;
223                }
224                return pos;
225        }
226
227
228        /**
229         * Actually parse the expression string and return an Expression object.
230         * @param expressionString the raw expression string to parse
231         * @param context a context for influencing this expression parsing routine (optional)
232         * @return an evaluator for the parsed expression
233         * @throws ParseException an exception occurred during parsing
234         */
235        protected abstract Expression doParseExpression(String expressionString, @Nullable ParserContext context)
236                        throws ParseException;
237
238
239        /**
240         * This captures a type of bracket and the position in which it occurs in the
241         * expression. The positional information is used if an error has to be reported
242         * because the related end bracket cannot be found. Bracket is used to describe:
243         * square brackets [] round brackets () and curly brackets {}
244         */
245        private static class Bracket {
246
247                char bracket;
248
249                int pos;
250
251                Bracket(char bracket, int pos) {
252                        this.bracket = bracket;
253                        this.pos = pos;
254                }
255
256                boolean compatibleWithCloseBracket(char closeBracket) {
257                        if (this.bracket == '{') {
258                                return closeBracket == '}';
259                        }
260                        else if (this.bracket == '[') {
261                                return closeBracket == ']';
262                        }
263                        return closeBracket == ')';
264                }
265
266                static char theOpenBracketFor(char closeBracket) {
267                        if (closeBracket == '}') {
268                                return '{';
269                        }
270                        else if (closeBracket == ']') {
271                                return '[';
272                        }
273                        return '(';
274                }
275
276                static char theCloseBracketFor(char openBracket) {
277                        if (openBracket == '{') {
278                                return '}';
279                        }
280                        else if (openBracket == '[') {
281                                return ']';
282                        }
283                        return ')';
284                }
285        }
286
287}