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}