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 }