001    /**
002     * Copyright 2005-2014 The Kuali Foundation
003     *
004     * Licensed under the Educational Community 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     * http://www.opensource.org/licenses/ecl2.php
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    package org.kuali.rice.krad.uif.util;
017    
018    import java.util.Deque;
019    
020    /**
021     * Provides modular support for the partial JSP EL path syntax using an arbitrary root bean as the
022     * initial name space.
023     * 
024     * <p>
025     * NOTE: This is not full JSP EL, only the path reference portion without support for floating point
026     * literals. See this <a href= "http://jsp.java.net/spec/jsp-2_1-fr-spec-el.pdf"> JSP Reference</a>
027     * for the full BNF.
028     * </p>
029     * 
030     * <pre>
031     * Value ::= ValuePrefix (ValueSuffix)*
032     * ValuePrefix ::= Literal
033     *     | NonLiteralValuePrefix
034     * NonLiteralValuePrefix ::= '(' Expression ')'
035     *     | Identifier
036     * ValueSuffix ::= '.' Identifier
037     *     | '[' Expression ']'
038     * Identifier ::= Java language identifier
039     * Literal ::= BooleanLiteral
040     *     | IntegerLiteral
041     *     | FloatingPointLiteral
042     *     | StringLiteral
043     *     | NullLiteral
044     * BooleanLiteral ::= 'true'
045     *     | 'false'
046     * StringLiteral ::= '([^'\]|\'|\\)*'
047     *     | "([^"\]|\"|\\)*"
048     *   i.e., a string of any characters enclosed by
049     *   single or double quotes, where \ is used to
050     *   escape ', ",and \. It is possible to use single
051     *   quotes within double quotes, and vice versa,
052     *   without escaping.
053     * IntegerLiteral ::= ['0'-'9']+
054     * NullLiteral ::= 'null'
055     * </pre>
056     * 
057     * @author Kuali Rice Team (rice.collab@kuali.org)
058     */
059    public class ObjectPathExpressionParser {
060    
061        /**
062         * Used by {@link #parsePathExpression(Object, String, PathEntry)} to track parse state without
063         * the need to construct a new parser stack for each expression.
064         */
065        private static final ThreadLocal<ParseState> TL_EL_PARSE_STATE = new ThreadLocal<ParseState>();
066    
067        /**
068         * Tracks parser state for
069         * {@link ObjectPathExpressionParser#parsePathExpression(Object, String, PathEntry)}.
070         */
071        private static final class ParseState {
072    
073            /**
074             * The continuation stack.
075             * <p>
076             * When evaluating subexpressions, the outer expression is pushed onto this stack.
077             * </p>
078             */
079            private final Deque<Object> stack = new java.util.LinkedList<Object>();
080    
081            /**
082             * The lexical index at which to begin the next lexical scan.
083             */
084            private int nextScanIndex;
085    
086            /**
087             * The lexical index of the next path separator token.
088             */
089            private int nextTokenIndex;
090    
091            /**
092             * The root continuation.
093             */
094            private Object root;
095    
096            /**
097             * The continuation point of the parse expression currently being evaluation.
098             */
099            private Object currentContinuation;
100    
101            /**
102             * Determine if this parse state is active.
103             */
104            private boolean isActive() {
105                return (stack != null && !stack.isEmpty()) || currentContinuation != null;
106            }
107    
108            /**
109             * Reset parse state, allowing this state marker to be reused on the next expression.
110             */
111            private void reset() {
112                stack.clear();
113                currentContinuation = null;
114            }
115    
116            /**
117             * Prepare for the next lexical scan.
118             * 
119             * <p>
120             * When a parenthetical expression occurs on the left hand side of the path, remove the
121             * parentheses.
122             * </p>
123             * 
124             * <p>
125             * Upon returning from this method, the value of {@link #nextScanIndex} will point at the
126             * position of the character formerly to the right of the removed parenthetical group, if
127             * applicable. If no parenthetical group was removed, {@link #nextScanIndex} will be reset
128             * to 0.
129             * </p>
130             * 
131             * @param path The path expression from the current continuation point.
132             * @return The path expression, with parentheses related to a grouping on the left removed.
133             */
134            public String prepareNextScan(String path) {
135                nextScanIndex = 0;
136    
137                char firstChar = path.charAt(0);
138    
139                if (firstChar != '(' && firstChar != '\'' && firstChar != '\"') {
140                    return path;
141                }
142    
143                int parenCount = firstChar == '(' ? 1 : 0;
144                int pathLen = path.length() - 1;
145    
146                // Look back during lexical scanning to detect quote and escape characers.
147                char lastChar = firstChar;
148                char currentChar;
149    
150                // Track quote state.
151                char inQuote = firstChar == '(' ? '\0' : firstChar;
152    
153                while (nextScanIndex < pathLen
154                        && ((firstChar == '(' && parenCount > 0) || (firstChar != '(' && inQuote != '\0'))) {
155                    nextScanIndex++;
156                    currentChar = path.charAt(nextScanIndex);
157    
158                    // Ignore escaped characters.
159                    if (lastChar == '\\') {
160                        continue;
161                    }
162    
163                    // Toggle quote state when a quote is encountered.
164                    if (inQuote == '\0') {
165                        if (currentChar == '\'' || currentChar == '\"') {
166                            inQuote = currentChar;
167                        }
168                    } else if (currentChar == inQuote) {
169                        // Emulate BeanWrapper bad quotes support. Automatically escape quotes where the close
170                        // quote is not positioned next to a path separator token.
171                        // i.e. aBean.aMap['aPoorly'quoted'key'] should reference "aPoorly'quoted'key" as the
172                        // key in the map.
173                        switch (nextScanIndex >= path.length() - 1 ? '\0' : path.charAt(nextScanIndex + 1)) {
174                        // Only accept close quote if followed by a lexical separator token.
175                            case ']':
176                            case '.':
177                            case '[':
178                                inQuote = '\0';
179                                break;
180                        }
181                    }
182    
183                    // Ignore quoted characters.
184                    if (inQuote != '\0') {
185                        continue;
186                    }
187    
188                    if (currentChar == '(') {
189                        parenCount++;
190                    }
191    
192                    if (currentChar == ')') {
193                        parenCount--;
194                    }
195                }
196    
197                if (parenCount > 0) {
198                    throw new IllegalArgumentException("Unmatched '(': " + path);
199                }
200    
201                if (firstChar == '(') {
202                    assert path.charAt(nextScanIndex) == ')';
203                    if (nextScanIndex < pathLen) {
204                        path = path.substring(1, nextScanIndex) + path.substring(nextScanIndex + 1);
205                    } else {
206                        path = path.substring(1, nextScanIndex);
207                    }
208                    nextScanIndex--;
209                } else {
210                    nextScanIndex++;
211                }
212    
213                return path;
214            }
215    
216            /**
217             * Update current parse state with the lexical indexes of the next token break.
218             * 
219             * @param path The path being parsed, starting from the current continuation point.
220             */
221            public void scan(String path) {
222                nextTokenIndex = -1;
223    
224                // Scan the character sequence, starting with the character following the open marker.
225                for (int currentIndex = nextScanIndex; currentIndex < path.length(); currentIndex++) {
226                    switch (path.charAt(currentIndex)) {
227                        case ')':
228                            // should have been removed by prepareNextScan
229                            throw new IllegalArgumentException("Unmatched ')': " + path);
230                        case '\'':
231                        case '\"':
232                        case '(':
233                        case '[':
234                        case '.':
235                        case ']':
236                            if (nextTokenIndex == -1) {
237                                nextTokenIndex = currentIndex;
238                            }
239                            return;
240                    }
241                }
242            }
243    
244            /**
245             * Step to the next continuation point in the parse path.
246             * 
247             * <p>
248             * Upon returning from this method, the value of {@link #currentContinuation} will reflect
249             * the resolved state of parsing the path. When null is returned, then
250             * {@link #currentContinuation} will be the reflect the result of parsing the expression.
251             * </p>
252             * 
253             * @param path The path expression from the current continuation point.
254             * @return The path expression for the next continuation point, null if the path has been
255             *         completely parsed.
256             */
257            private String step(String path, PathEntry pathEntry) {
258    
259                if (nextTokenIndex == -1) {
260                    // Only a symbolic reference, resolve it and return.
261                    currentContinuation = pathEntry.parse(pathEntry.prepare(currentContinuation), path, false);
262                    return null;
263                }
264    
265                char nextToken = path.charAt(nextTokenIndex);
266    
267                switch (nextToken) {
268    
269                    case '[':
270                        // Entering bracketed subexpression
271    
272                        // Resolve non-empty key reference as the current continuation
273                        if (nextTokenIndex != 0) {
274                            currentContinuation = pathEntry.parse(
275                                    pathEntry.prepare(currentContinuation),
276                                    path.substring(0, nextTokenIndex), false);
277                        }
278    
279                        // Push current continuation down in the stack.
280                        stack.push(currentContinuation);
281    
282                        // Reset the current continuation for evaluating the
283                        // subexpression
284                        currentContinuation = pathEntry.parse(root, null, false);
285                        return path.substring(nextTokenIndex + 1);
286    
287                    case '(':
288                        // Approaching a parenthetical expression, not preceded by a subexpression,
289                        // resolve the key reference as the current continuation
290                        currentContinuation = pathEntry.parse(pathEntry.prepare(currentContinuation),
291                                path.substring(0, nextTokenIndex), false);
292                        return path.substring(nextTokenIndex); // Keep the left parenthesis
293    
294                    case '.':
295                        // Crossing a period, not preceded by a subexpression,
296                        // resolve the key reference as the current continuation
297                        currentContinuation = pathEntry.parse(pathEntry.prepare(currentContinuation),
298                                path.substring(0, nextTokenIndex), false);
299                        return path.substring(nextTokenIndex + 1); // Skip the period
300    
301                    case ']':
302                        if (nextTokenIndex > 0) {
303                            // Approaching a right bracket, resolve the key reference as the current continuation
304                            currentContinuation = pathEntry.parse(pathEntry.prepare(currentContinuation),
305                                    path.substring(0, nextTokenIndex), false);
306                            return path.substring(nextTokenIndex); // Keep the right bracket
307    
308                        } else {
309                            // Crossing a right bracket.
310    
311                            // Use the current continuation as the parameter for resolving
312                            // the top continuation on the stack, then make the result the
313                            // current continuation.
314                            currentContinuation = pathEntry.parse(pathEntry.prepare(stack.pop()),
315                                    pathEntry.dereference(currentContinuation), true);
316                            if (nextTokenIndex + 1 < path.length()) {
317                                // short-circuit the next step, as an optimization for
318                                // handling dot resolution without permitting double-dots
319                                switch (path.charAt(nextTokenIndex + 1)) {
320                                    case '.':
321                                        // crossing a dot, skip it
322                                        return path.substring(nextTokenIndex + 2);
323                                    case '[':
324                                    case ']':
325                                        // crossing to another subexpression, don't skip it.
326                                        return path.substring(nextTokenIndex + 1);
327                                    default:
328                                        throw new IllegalArgumentException(
329                                                "Expected '.', '[', or ']': " + path);
330                                }
331                            } else {
332                                return null;
333                            }
334                        }
335    
336                    default:
337                        throw new IllegalArgumentException("Unexpected '" + nextToken + "' :" + path);
338                }
339            }
340    
341        }
342    
343        /**
344         * Path entry interface for use with
345         * {@link ObjectPathExpressionParser#parsePathExpression(Object, String, PathEntry)}.
346         */
347        public static interface PathEntry {
348    
349            /**
350             * Parse one node.
351             * 
352             * @param node The current parse node.
353             * @param next The next path token.
354             * @param inherit True indicates that the current node is the result of a subexpression,
355             *        false indicates the next node in the same expression.
356             * @return A reference to the next parse node.
357             */
358            Object parse(Object node, String next, boolean inherit);
359    
360            /**
361             * Prepare the next parse node based on a reference returned from the prior node.
362             * 
363             * @param prev The reference data from the previous node.
364             * @return The next parse node.
365             */
366            Object prepare(Object prev);
367    
368            /**
369             * Resolve the next path element based on reference data from the previous node.
370             * 
371             * @param prev The reference data from the previous node.
372             * @return the next path element based on the returned reference data.
373             */
374            String dereference(Object prev);
375        }
376    
377        /**
378         * Parse a path expression.
379         * 
380         * @param root The root object.
381         * @param path The path expression.
382         * @param pathEntry The path entry adaptor to use for processing parse node transition.
383         * @param <T> Reference type representing the next parse node.
384         * @param <S> The parse node type.
385         * 
386         * @return The valid of the bean property indicated by the given path expression, null if the
387         *         path expression doesn't resolve to a valid property.
388         * @see ObjectPathExpressionParser#getPropertyValue(Object, String)
389         */
390        public static Object parsePathExpression(Object root, String path,
391                final PathEntry pathEntry) {
392    
393            // NOTE: This iterative parser allows support for subexpressions
394            // without recursion. When a subexpression start token '[' is
395            // encountered the current continuation is pushed onto a stack. When
396            // the subexpression is resolved, the continuation is popped back
397            // off the stack and resolved using the subexpression result as the
398            // arg. All subexpressions start with the same root passed in as an
399            // argument for this method. - MWF
400    
401            ParseState parseState = (ParseState) TL_EL_PARSE_STATE.get();
402            boolean recycle;
403    
404            if (parseState == null) {
405                TL_EL_PARSE_STATE.set(new ParseState());
406                parseState = TL_EL_PARSE_STATE.get();
407                recycle = true;
408            } else if (parseState.isActive()) {
409                ProcessLogger.ntrace("el-parse:", ":nested", 100);
410                parseState = new ParseState();
411                recycle = false;
412            } else {
413                recycle = true;
414            }
415    
416            try {
417                parseState.root = root;
418                parseState.currentContinuation = pathEntry.parse(root, null, false);
419                while (path != null) {
420                    path = parseState.prepareNextScan(path);
421                    parseState.scan(path);
422                    path = parseState.step(path, pathEntry);
423                }
424                return parseState.currentContinuation;
425            } finally {
426                assert !recycle || parseState == TL_EL_PARSE_STATE.get();
427                parseState.reset();
428            }
429        }
430    
431        /**
432         * Private constructor - utility class only.
433         */
434        private ObjectPathExpressionParser() {}
435    
436    }