001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 the ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| 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+-------------------------------------------------------------------------*/
017package eu.cqse.check.framework.shallowparser.util;
018
019import static eu.cqse.check.framework.shallowparser.util.EntitySelectionExpressionParsingException.EParsingExceptionMessage.EXPECTED_BINARY_OPERATOR;
020import static eu.cqse.check.framework.shallowparser.util.EntitySelectionExpressionParsingException.EParsingExceptionMessage.EXPECTED_EXPRESSION;
021import static eu.cqse.check.framework.shallowparser.util.EntitySelectionExpressionParsingException.EParsingExceptionMessage.MISPLACED_CLOSING_PARENTHESIS;
022import static eu.cqse.check.framework.shallowparser.util.EntitySelectionExpressionParsingException.EParsingExceptionMessage.MISSING_CLOSING_PARENTHESIS;
023import static eu.cqse.check.framework.shallowparser.util.EntitySelectionExpressionParsingException.EParsingExceptionMessage.PARAMETER_MISSING;
024import static eu.cqse.check.framework.shallowparser.util.EntitySelectionExpressionParsingException.EParsingExceptionMessage.PREDICATE_CONSTRUCTION_FAILED;
025import static eu.cqse.check.framework.shallowparser.util.EntitySelectionExpressionParsingException.EParsingExceptionMessage.PREDICATE_NOT_FOUND;
026import static eu.cqse.check.framework.shallowparser.util.EntitySelectionExpressionParsingException.EParsingExceptionMessage.UNEXPECTED_CHARACTER;
027import static eu.cqse.check.framework.shallowparser.util.EntitySelectionExpressionParsingException.EParsingExceptionMessage.UNSUPPORTED_PARAMETER;
028
029import java.lang.reflect.InvocationTargetException;
030import java.lang.reflect.Method;
031import java.lang.reflect.Modifier;
032import java.util.HashMap;
033import java.util.Map;
034import java.util.function.Predicate;
035
036import org.conqat.lib.commons.assertion.CCSMAssert;
037import org.conqat.lib.commons.string.StringUtils;
038
039import eu.cqse.check.framework.shallowparser.ShallowParserException;
040import eu.cqse.check.framework.shallowparser.framework.ShallowEntity;
041import eu.cqse.check.framework.shallowparser.util.EntitySelectionExpressionParsingException.EParsingExceptionMessage;
042
043/**
044 * Parses expressions for selecting {@link ShallowEntity}s from a parse tree.
045 * The expressions may be formed by '&', '|', '!', parentheses, and all
046 * predicates defined in {@link EntitySelectionPredicates} (i.e. you can use all
047 * public methods defined there without respect of case). Predicates may contain
048 * additional underscores and dashes to improve readability, so 'simpleGetter',
049 * 'simplegetter', 'simple_getter' and 'simple-getter' would all be treated the
050 * same.
051 * 
052 * An example for an expression that matches all public attributes and methods,
053 * but no simple setters/getter and no methods annotated with '@Override' would
054 * be
055 * 
056 * <pre>
057 *     public & (attribute | method) & !(simple-getter | simple-setter | annotated(override))
058 * </pre>
059 */
060public class EntitySelectionExpressionParser {
061
062        /** The available factory methods by normalized name. */
063        private static Map<String, Method> factoryMethods;
064
065        /** The expression to parse. */
066        private final String expression;
067
068        /** The current position into {@link #expression} (during parsing). */
069        private int position = 0;
070
071        /** Constructor. */
072        private EntitySelectionExpressionParser(String expression) {
073                if (factoryMethods == null) {
074                        factoryMethods = loadFactoryMethods();
075                }
076
077                this.expression = expression;
078        }
079
080        /**
081         * Returns a map of factory methods extracted from
082         * {@link EntitySelectionPredicates}.
083         */
084        private static Map<String, Method> loadFactoryMethods() {
085                Map<String, Method> methods = new HashMap<String, Method>();
086                for (Method method : EntitySelectionPredicates.class.getDeclaredMethods()) {
087                        if (!Modifier.isPublic(method.getModifiers()) || !Modifier.isStatic(method.getModifiers())) {
088                                continue;
089                        }
090
091                        assertParameterTypes(method);
092
093                        methods.put(normalizeName(method.getName()), method);
094                }
095                return methods;
096        }
097
098        /**
099         * Raises an assertion error if the method has more then one parameter or
100         * its single parameter is not a string.
101         */
102        private static void assertParameterTypes(Method method) {
103                Class<?>[] parameterTypes = method.getParameterTypes();
104                boolean noParameters = parameterTypes.length == 0;
105                boolean oneStringParameter = parameterTypes.length == 1 && parameterTypes[0] == String.class;
106                CCSMAssert.isTrue(noParameters || oneStringParameter,
107                                "Factory methods in " + EntitySelectionPredicates.class.getSimpleName()
108                                                + " must have no parameters or one string parameter.");
109        }
110
111        /** Parses the given expression and returns a corresponding predicate. */
112        public static Predicate<ShallowEntity> parse(String expression) throws ShallowParserException {
113                return new EntitySelectionExpressionParser(expression).parse();
114        }
115
116        /** Parses the {@link #expression}. */
117        private Predicate<ShallowEntity> parse() throws ShallowParserException {
118                Predicate<ShallowEntity> predicate = parse(false, true);
119                if (position < expression.length()) {
120                        error(MISPLACED_CLOSING_PARENTHESIS);
121                }
122                return predicate;
123        }
124
125        /**
126         * Parses the {@link #expression}.
127         * 
128         * @param expectClosingParenthesis
129         *            if this is true, the parse was started from an opening
130         *            parenthesis and a closing parenthesis would be expected at the
131         *            end of the local expression.
132         * @param mayParseBinary
133         *            if this is true, parse not only a single term but potentially
134         *            continue with parsing binary operators. This is used to
135         *            implement a very simple operator precedence for '!'.
136         */
137        private Predicate<ShallowEntity> parse(boolean expectClosingParenthesis, boolean mayParseBinary)
138                        throws ShallowParserException {
139
140                Predicate<ShallowEntity> result = null;
141                while (position < expression.length()) {
142                        char next = expression.charAt(position++);
143                        if (Character.isWhitespace(next)) {
144                                continue;
145                        }
146
147                        if (isIdentifierCharacter(next)) {
148                                assertNoResult(result);
149                                position -= 1;
150                                result = parsePrimitiveExpression();
151                                if (!mayParseBinary) {
152                                        return result;
153                                }
154                                continue;
155                        }
156
157                        switch (next) {
158                        case '(':
159                                assertNoResult(result);
160                                result = parse(true, true);
161                                break;
162
163                        case ')':
164                                if (!expectClosingParenthesis) {
165                                        position -= 1; // leave for one of the outer calls
166                                }
167                                return assertResult(result);
168
169                        case '&':
170                                if (!mayParseBinary) {
171                                        position -= 1; // leave for outer call to handle
172                                        return assertResult(result);
173                                }
174                                result = assertResult(result).and(parse(false, true));
175                                break;
176
177                        case '|':
178                                if (!mayParseBinary) {
179                                        position -= 1; // leave for outer call to handle
180                                        return assertResult(result);
181                                }
182                                result = assertResult(result).or(parse(false, true));
183                                break;
184
185                        case '!':
186                                assertNoResult(result);
187                                result = parse(false, false).negate();
188                                break;
189
190                        default:
191                                error(UNEXPECTED_CHARACTER);
192                        }
193                }
194
195                if (expectClosingParenthesis) {
196                        error(MISSING_CLOSING_PARENTHESIS);
197                }
198                return assertResult(result);
199        }
200
201        /** Parses a primitive expression. */
202        private Predicate<ShallowEntity> parsePrimitiveExpression() throws ShallowParserException {
203                StringBuilder nameBuilder = new StringBuilder();
204                while (position < expression.length() && isIdentifierCharacter(expression.charAt(position))) {
205                        nameBuilder.append(expression.charAt(position++));
206                }
207
208                return createPredicate(normalizeName(nameBuilder.toString()), extractParameter());
209        }
210
211        /**
212         * Extracts a predicate parameter or returns null if no parameter is
213         * provided.
214         */
215        private String extractParameter() throws EntitySelectionExpressionParsingException {
216                // skip whitespace
217                while (position < expression.length() && Character.isWhitespace(expression.charAt(position))) {
218                        position += 1;
219                }
220
221                if (position >= expression.length() || expression.charAt(position) != '(') {
222                        return null;
223                }
224
225                StringBuilder parameterBuilder = new StringBuilder();
226                position += 1;
227                int nestingCount = 0;
228                while (position < expression.length() && (nestingCount > 0 || expression.charAt(position) != ')')) {
229                        char next = expression.charAt(position);
230                        if (next == '(') {
231                                nestingCount += 1;
232                        } else if (next == ')') {
233                                nestingCount -= 1;
234                        }
235                        parameterBuilder.append(next);
236                        position += 1;
237                }
238
239                if (position >= expression.length()) {
240                        error(MISSING_CLOSING_PARENTHESIS);
241                }
242                position += 1;
243
244                if (parameterBuilder.length() == 0) {
245                        return null;
246                }
247
248                return parameterBuilder.toString().replaceAll("['\"]", StringUtils.EMPTY_STRING);
249        }
250
251        /**
252         * Normalizes the predicate name. This removes the prefix "select", makes
253         * the text lowercase, and discards all underscores and dashes.
254         */
255        private static String normalizeName(String name) {
256                name = name.toLowerCase();
257                name = name.replaceAll("[-_]", StringUtils.EMPTY_STRING);
258                name = StringUtils.stripPrefix(name, "select");
259                return name;
260        }
261
262        /**
263         * Returns whether the given character is expected to occur in identifiers,
264         * i.e. is alphanumeric, an underscore, or a dash.
265         */
266        private static boolean isIdentifierCharacter(char character) {
267                return Character.isJavaIdentifierPart(character) || character == '-';
268        }
269
270        /**
271         * Creates a predicate of given (normalized) name. This never returns null.
272         * 
273         * @param parameter
274         *            the parameter to the predicate. May be null to indicate a
275         *            parameterless predicate.
276         */
277        @SuppressWarnings("unchecked")
278        private Predicate<ShallowEntity> createPredicate(String name, String parameter) throws ShallowParserException {
279                Method method = factoryMethods.get(name);
280
281                try {
282                        if (method == null) {
283                                error(PREDICATE_NOT_FOUND);
284                        } else if (method.getParameterTypes().length == 0) {
285                                if (parameter != null) {
286                                        error(UNSUPPORTED_PARAMETER);
287                                }
288                                return (Predicate<ShallowEntity>) method.invoke(null);
289                        } else {
290                                if (parameter == null) {
291                                        error(PARAMETER_MISSING);
292                                }
293                                return (Predicate<ShallowEntity>) method.invoke(null, parameter);
294                        }
295                } catch (IllegalAccessException e) {
296                        error(PREDICATE_CONSTRUCTION_FAILED, e);
297                } catch (InvocationTargetException e) {
298                        if (e.getCause() instanceof ShallowParserException) {
299                                throw (ShallowParserException) e.getCause();
300                        }
301                        error(PREDICATE_CONSTRUCTION_FAILED, e.getCause());
302                }
303                throw new AssertionError("This line should not be reachable!");
304        }
305
306        /**
307         * Asserts that the result is still null, i.e. there has been no previous
308         * expression at this level. The error message hence reports a missing
309         * binary operator.
310         */
311        private void assertNoResult(Predicate<ShallowEntity> result) throws EntitySelectionExpressionParsingException {
312                if (result != null) {
313                        error(EXPECTED_BINARY_OPERATOR);
314                }
315        }
316
317        /**
318         * Asserts that the result is not null, i.e. there has been a previous
319         * expression at this level. The error message hence reports a missing
320         * previous expression. Returns the parameter for convenience.
321         */
322        private Predicate<ShallowEntity> assertResult(Predicate<ShallowEntity> result)
323                        throws EntitySelectionExpressionParsingException {
324                if (result == null) {
325                        error(EXPECTED_EXPRESSION);
326                }
327                return result;
328        }
329
330        /**
331         * Throws an exception with given message and details on the current parsing
332         * position.
333         */
334        private void error(EParsingExceptionMessage messageIdentifier) throws EntitySelectionExpressionParsingException {
335                throw new EntitySelectionExpressionParsingException(messageIdentifier, expression, position);
336        }
337
338        /**
339         * Throws an exception with given message and details on the current parsing
340         * position.
341         */
342        private void error(EParsingExceptionMessage messageIdentifier, Throwable cause)
343                        throws EntitySelectionExpressionParsingException {
344                throw new EntitySelectionExpressionParsingException(messageIdentifier, expression, position, cause);
345        }
346}