/* Copyright 2005-2006 Tim Fennell * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.sourceforge.stripes.util.bean; import java.util.regex.Pattern; import java.util.regex.Matcher; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * <p>An expression representing a property, nested property or indexed property of a JavaBean, or * a combination of all three. Capable of parsing String property expressions into a series of * {@link Node}s representing each sub-property or indexed property. Expression nodes can be * separated with periods, or square-bracket indexing. Items inside square brackets can be * single or double quoted, or bare int/long/float/double/boolean literals in the same manner they * appear in Java source code (e.g. 123.6F for a float).</p> * * @author Tim Fennell * @since Stripes 1.4 */ public class PropertyExpression { // Patterns used to identify the type of Nodes in expressions private static final Pattern REGEX_INTEGER = Pattern.compile("^-?\\d+$"); private static final Pattern REGEX_LONG = Pattern.compile("^(-?\\d+)L$", Pattern.CASE_INSENSITIVE); private static final Pattern REGEX_DOUBLE = Pattern.compile("^-?\\d+\\.\\d+$"); private static final Pattern REGEX_FLOAT = Pattern.compile("^(-?\\d+\\.?\\d+)F$", Pattern.CASE_INSENSITIVE); private static final Pattern REGEX_BOOLEAN = Pattern.compile("^(true|false)$", Pattern.CASE_INSENSITIVE); /** The set of characters which can terminate an expression node in one way or another. */ private static final String TERMINATOR_CHARS = ".[]"; /** A static cache of parse expressions. */ private static Map<String,PropertyExpression> expressions = new ConcurrentHashMap<String,PropertyExpression>(); /** The original property string, or 'source' of the expression. */ private String source; private Node root; private Node leaf; /** Constructs a new expression by parsing the supplied String. */ private PropertyExpression(String expression) throws ParseException { this.source = expression; parse(expression); } /** * Fetches the root or first node in this expression. In an expression like 'foo.bar' this * would return the node that contains 'foo'. * @return the first node in the expression */ public Node getRootNode() { return this.root; } /** * Fetches the original 'source' of the expression - the String value that was parsed * to create the PropertyExpression object. * @return the String form of the expression that was parsed */ public String getSource() { return source; } /** * Factory method for retrieving PropertyExpression objects for expression strings. * * @param expression the expression to fetch a PropertyExpression for * @return PropertyExpression the parsed form of the expression passed in */ public static PropertyExpression getExpression(String expression) throws ParseException { PropertyExpression parsed = PropertyExpression.expressions.get(expression); if (parsed == null) { parsed = new PropertyExpression(expression); PropertyExpression.expressions.put(expression, parsed); } return parsed; } /** * Performs the internal parsing of the expression and stores the results in a chain * of nodes internally. Passes through the String a character at a time looking for * transitions between nodes and invalid states. * * @param expression the String expression to be parsed */ protected void parse(String expression) throws ParseException { char[] chars = expression.toCharArray(); StringBuilder builder = new StringBuilder(); boolean inSingleQuotedString = false; boolean inDoubleQuotedString = false; boolean inSquareBrackets = false; boolean escapedChar = false; for (int i=0; i<chars.length; ++i) { char ch = chars[i]; // If the previous char was an escape char, accept the next char no questions asked if (escapedChar) { builder.append(ch); escapedChar = false; } // If it's the escape char, record it and skip to the next char else if (ch == '\\') { escapedChar = true; } // Deal with single quotes else if (!inSingleQuotedString && ch == '\'') { inSingleQuotedString = true; } else if (inSingleQuotedString && ch == '\'') { inSingleQuotedString = false; // assert that we're at the end of the expression, or the next char is a [ or . if (i != chars.length -1 && TERMINATOR_CHARS.indexOf(chars[i+1]) == -1) { throw new ParseException("A quoted String must be terminated by a matching " + "quote followed by either the end of the expression, a period or a " + "square bracket character.", expression); } else { String value = builder.toString(); addNode(value, value.length() == 1 ? value.charAt(0) : value, inSquareBrackets); builder.setLength(0); } } else if (inSingleQuotedString) { builder.append(ch); } // Deal with Double quotes else if (!inDoubleQuotedString && ch == '"') { inDoubleQuotedString = true; } else if (inDoubleQuotedString && ch == '"') { inDoubleQuotedString = false; // assert that we're at the end of the expression, or the next char is a [ or . if (i != chars.length -1 && TERMINATOR_CHARS.indexOf(chars[i+1]) == -1) { throw new ParseException("A quoted String must be terminated by a matching " + "quote followed by either the end of the expression, a period or a " + "square bracket character.", expression); } else { String value = builder.toString(); addNode(value, value, inSquareBrackets); builder.setLength(0); } } else if (inDoubleQuotedString) { builder.append(ch); } // Deal with square brackets else if (!inSquareBrackets && ch == '[') { if (builder.length() > 0) { addNode(builder.toString(), null, inSquareBrackets); builder.setLength(0); } inSquareBrackets = true; } else if (inSquareBrackets) { // Using the nested IF allows us to consume periods in unquoted strings of digits if (ch == ']') { if (builder.length() > 0) { addNode(builder.toString(), null, inSquareBrackets); builder.setLength(0); } inSquareBrackets = false; } else { builder.append(ch); } } // If it's a bare period, it's the end of the current node else if (ch == '.') { if (builder.length() < 1) { // Ignore pseudo-zero-length nodes } else { addNode(builder.toString(), null, inSquareBrackets); builder = new StringBuilder(); } } else { builder.append(ch); } // If it's the last char and we have stuff in buffer, close out the last node if (i == chars.length - 1) { if (inSingleQuotedString) { throw new ParseException(expression, "Expression appears to terminate inside of single quoted string."); } else if (inDoubleQuotedString) { throw new ParseException(expression, "Expression appears to terminate inside of double quoted string."); } else if (inSquareBrackets) { throw new ParseException(expression, "Expression appears to terminate inside of square bracketed sub-expression."); } else if (builder.length() > 0) { addNode(builder.toString(), null, inSquareBrackets); } } } } /** * Constructs a node and links it in to other nodes within the current expression. * @param nodeValue the String part of the expression that the node represents * @param typedValue a strongly typed value for the nodeValue if one is indicated by * the expression String, otherwise null to automatically determine * @param bracketed True if {@code nodeValue} was inside square brackets. */ private void addNode(String nodeValue, Object typedValue, boolean bracketed) { // Determine the primitive/wrapper type of the node if (typedValue != null) { // skip ahead } else if (REGEX_INTEGER.matcher(nodeValue).matches()) { typedValue = Integer.parseInt(nodeValue); } else if (REGEX_DOUBLE.matcher(nodeValue).matches()) { typedValue = Double.parseDouble(nodeValue); } else if (REGEX_LONG.matcher(nodeValue).matches()) { Matcher matcher = REGEX_LONG.matcher(nodeValue); matcher.matches(); typedValue = Long.parseLong(matcher.group(1)); } else if (REGEX_FLOAT.matcher(nodeValue).matches()) { Matcher matcher = REGEX_FLOAT.matcher(nodeValue); matcher.find(); typedValue = Float.parseFloat(matcher.group(1)); } else if (REGEX_BOOLEAN.matcher(nodeValue).matches()) { typedValue = Boolean.parseBoolean(nodeValue); } else { typedValue = nodeValue; } Node node = new Node(nodeValue, typedValue, bracketed); // Attach the node at the appropriate point in the expression if (this.root == null) { this.root = this.leaf = node; } else { node.setPrevious(this.leaf); this.leaf.setNext(node); this.leaf = node; } } /** Returns the String expression that was parsed to create this PropertyExpression. */ @Override public String toString() { return this.source; } }