diff --git a/client-java/controller/pom.xml b/client-java/controller/pom.xml index e599727ab4..93965fff82 100644 --- a/client-java/controller/pom.xml +++ b/client-java/controller/pom.xml @@ -140,6 +140,13 @@ provided + + + org.antlr + antlr4-runtime + + + com.h2database h2 @@ -204,10 +211,15 @@ lettuce-core test - software.amazon.awssdk - netty-nio-client + dynamodb + test + + + + org.slf4j + slf4j-simple test @@ -227,7 +239,6 @@ grpc-stub test - @@ -265,6 +276,23 @@ maven-compiler-plugin + + + org.antlr + antlr4-maven-plugin + + false + true + + + + + antlr4 + + + + + + + org.antlr + shaded.org.antlr + + diff --git a/client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/dynamodb/DynamoDbConditionExpression.g4 b/client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/dynamodb/DynamoDbConditionExpression.g4 new file mode 100644 index 0000000000..81b72a6d5c --- /dev/null +++ b/client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/dynamodb/DynamoDbConditionExpression.g4 @@ -0,0 +1,123 @@ +grammar DynamoDbConditionExpression; + +expression + : orExpr EOF + ; + +orExpr + : andExpr (OR andExpr)* + ; + +andExpr + : notExpr (AND notExpr)* + ; + +notExpr + : NOT notExpr #negatedExpr + | primary #primaryExpr + ; + +primary + : LPAREN orExpr RPAREN #parenthesizedPrimary + | predicate #predicatePrimary + ; + +predicate + : ATTRIBUTE_EXISTS LPAREN path RPAREN #attributeExistsPredicate + | ATTRIBUTE_NOT_EXISTS LPAREN path RPAREN #attributeNotExistsPredicate + | ATTRIBUTE_TYPE LPAREN path COMMA value RPAREN #attributeTypePredicate + | BEGINS_WITH LPAREN path COMMA value RPAREN #beginsWithPredicate + | CONTAINS LPAREN path COMMA value RPAREN #containsPredicate + | SIZE LPAREN path RPAREN comparator value #sizePredicate + | path BETWEEN value AND value #betweenPredicate + | path IN LPAREN value (COMMA value)* RPAREN #inPredicate + | path comparator value #comparisonPredicate + ; + +comparator + : EQ + | NE + | LT + | LTE + | GT + | GTE + ; + +path + : IDENTIFIER + ; + +value + : PLACEHOLDER #placeholderValue + | STRING_LITERAL #stringValue + | NUMBER_LITERAL #numberValue + | BOOLEAN_LITERAL #booleanValue + | NULL_LITERAL #nullValue + | IDENTIFIER #identifierValue + ; + +LPAREN : '('; +RPAREN : ')'; +COMMA : ','; +EQ : '='; +NE : '<>'; +LTE : '<='; +GTE : '>='; +LT : '<'; +GT : '>'; + +AND : A N D; +OR : O R; +NOT : N O T; +BETWEEN : B E T W E E N; +IN : I N; +ATTRIBUTE_EXISTS : A T T R I B U T E '_' E X I S T S; +ATTRIBUTE_NOT_EXISTS : A T T R I B U T E '_' N O T '_' E X I S T S; +ATTRIBUTE_TYPE : A T T R I B U T E '_' T Y P E; +BEGINS_WITH : B E G I N S '_' W I T H; +CONTAINS : C O N T A I N S; +SIZE : S I Z E; + +BOOLEAN_LITERAL : T R U E | F A L S E; +NULL_LITERAL : N U L L; + +PLACEHOLDER : ':' IDENT_START IDENT_PART*; +NUMBER_LITERAL : '-'? DIGIT+ ('.' DIGIT+)? EXPONENT?; +STRING_LITERAL : '\'' ~['\r\n]* '\''; + +IDENTIFIER : IDENT_START IDENT_PART* INDEX* ('.' IDENT_START IDENT_PART* INDEX*)*; + +WS : [ \t\r\n]+ -> skip; + +fragment INDEX : '[' DIGIT+ ']'; +fragment EXPONENT : [eE] [+\-]? DIGIT+; +fragment IDENT_START : [a-zA-Z_#]; +fragment IDENT_PART : [a-zA-Z0-9_]; +fragment DIGIT : [0-9]; + +fragment A : [aA]; +fragment B : [bB]; +fragment C : [cC]; +fragment D : [dD]; +fragment E : [eE]; +fragment F : [fF]; +fragment G : [gG]; +fragment H : [hH]; +fragment I : [iI]; +fragment J : [jJ]; +fragment K : [kK]; +fragment L : [lL]; +fragment M : [mM]; +fragment N : [nN]; +fragment O : [oO]; +fragment P : [pP]; +fragment Q : [qQ]; +fragment R : [rR]; +fragment S : [sS]; +fragment T : [tT]; +fragment U : [uU]; +fragment V : [vV]; +fragment W : [wW]; +fragment X : [xX]; +fragment Y : [yY]; +fragment Z : [zZ]; diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbAttributeValueHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbAttributeValueHelper.java new file mode 100644 index 0000000000..1ac2a78c64 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbAttributeValueHelper.java @@ -0,0 +1,231 @@ +package org.evomaster.client.java.controller.dynamodb; + +import java.nio.ByteBuffer; +import java.util.*; + +/** + * Utilities to deal with DynamoDB SDK request/response values without + * introducing direct compile-time dependencies to AWS SDK classes. + */ +public final class DynamoDbAttributeValueHelper { + + /** + * Reflection-bound AWS AttributeValue accessors. Keep these literals unchanged: + * they must match SDK method names exactly. + */ + private static final String METHOD_NUL = "nul"; + private static final String METHOD_S = "s"; + private static final String METHOD_N = "n"; + private static final String METHOD_BOOL = "bool"; + private static final String METHOD_HAS_M = "hasM"; + private static final String METHOD_M = "m"; + private static final String METHOD_HAS_L = "hasL"; + private static final String METHOD_L = "l"; + private static final String METHOD_HAS_SS = "hasSs"; + private static final String METHOD_SS = "ss"; + private static final String METHOD_HAS_NS = "hasNs"; + private static final String METHOD_NS = "ns"; + private static final String METHOD_HAS_BS = "hasBs"; + private static final String METHOD_BS = "bs"; + private static final String METHOD_B = "b"; + + private static final String DECIMAL_SEPARATOR = "."; + private static final String SCIENTIFIC_NOTATION_E_LOWER = "e"; + private static final String SCIENTIFIC_NOTATION_E_UPPER = "E"; + + /** + * Utility class, no instances. + */ + private DynamoDbAttributeValueHelper() { + } + + /** + * Converts a map of DynamoDB attribute values into plain Java values. + * + * @param source input object expected to be a map + * @return normalized map or empty map when input is not a map + */ + public static Map toPlainMap(Object source) { + if (!(source instanceof Map)) { + return Collections.emptyMap(); + } + + Map result = new LinkedHashMap<>(); + ((Map) source).forEach((key, value) -> { + if (key != null) { + result.put(String.valueOf(key), toPlainValue(value)); + } + }); + return result; + } + + /** + * Converts one DynamoDB attribute value object into a plain Java value. + * + * @param value attribute value object + * @return normalized Java value + */ + @SuppressWarnings("unchecked") + public static Object toPlainValue(Object value) { + if (value == null) { + return null; + } + + if (value instanceof Map) { + return toPlainMap(value); + } + + if (value instanceof Collection) { + return toPlainList((Collection) value); + } + + // The AWS SDK AttributeValue class exposes "hasXxx"/"xxx" methods. + // We use reflection to stay decoupled from specific SDK versions. + Object nul = DynamoDbReflectionHelper.invokeBooleanNoArg(value, METHOD_NUL); + if (Boolean.TRUE.equals(nul)) { + return null; + } + + Object s = DynamoDbReflectionHelper.invokeNoArg(value, METHOD_S); + if (s instanceof String) { + return s; + } + + Object n = DynamoDbReflectionHelper.invokeNoArg(value, METHOD_N); + if (n instanceof String && !((String) n).isEmpty()) { + return parseNumber((String) n); + } + + Object bool = DynamoDbReflectionHelper.invokeNoArg(value, METHOD_BOOL); + if (bool instanceof Boolean) { + return bool; + } + + Object m = readIfPresent(value, METHOD_HAS_M, METHOD_M); + if (m instanceof Map) { + return toPlainMap(m); + } + + Object l = readIfPresent(value, METHOD_HAS_L, METHOD_L); + if (l instanceof Collection) { + return toPlainList((Collection) l); + } + + Object ss = readIfPresent(value, METHOD_HAS_SS, METHOD_SS); + if (ss instanceof Collection) { + return new LinkedHashSet<>((Collection) ss); + } + + Object ns = readIfPresent(value, METHOD_HAS_NS, METHOD_NS); + if (ns instanceof Collection) { + return toNumberSet((Collection) ns); + } + + Object bs = readIfPresent(value, METHOD_HAS_BS, METHOD_BS); + if (bs instanceof Collection) { + return toBinarySet((Collection) bs); + } + + Object b = DynamoDbReflectionHelper.invokeNoArg(value, METHOD_B); + if (b != null) { + return toPlainBinary(b); + } + + return value; + } + + /** + * Converts binary payloads into plain byte arrays when backed by ByteBuffer. + * + * @param value binary payload object + * @return byte array or original value when conversion is not needed + */ + private static Object toPlainBinary(Object value) { + if (value instanceof ByteBuffer) { + ByteBuffer bb = ((ByteBuffer) value).asReadOnlyBuffer(); + byte[] bytes = new byte[bb.remaining()]; + bb.get(bytes); + return bytes; + } + + return value; + } + + /** + * Reads a reflected value only when its corresponding {@code hasX} accessor is true. + * + * @param target target object + * @param hasMethod presence-check method name + * @param valueMethod value accessor method name + * @return reflected value or {@code null} + */ + private static Object readIfPresent(Object target, String hasMethod, String valueMethod) { + if (Boolean.TRUE.equals(DynamoDbReflectionHelper.invokeBooleanNoArg(target, hasMethod))) { + return DynamoDbReflectionHelper.invokeNoArg(target, valueMethod); + } + return null; + } + + /** + * Converts a collection of attribute values into plain Java values. + * + * @param source source collection + * @return normalized list + */ + private static List toPlainList(Collection source) { + List converted = new ArrayList<>(source.size()); + for (Object element : source) { + converted.add(toPlainValue(element)); + } + return converted; + } + + /** + * Converts a collection of numeric tokens into parsed numeric values. + * + * @param source source numeric collection + * @return normalized number set + */ + private static Set toNumberSet(Collection source) { + LinkedHashSet numbers = new LinkedHashSet<>(); + for (Object number : source) { + if (number != null) { + numbers.add(parseNumber(String.valueOf(number))); + } + } + return numbers; + } + + /** + * Converts a collection of binary payloads into plain binary values. + * + * @param source source binary collection + * @return normalized binary set + */ + private static Set toBinarySet(Collection source) { + LinkedHashSet binaries = new LinkedHashSet<>(); + for (Object binary : source) { + binaries.add(toPlainBinary(binary)); + } + return binaries; + } + + /** + * Parses a numeric token into {@link Long} or {@link Double}. + * + * @param text numeric token + * @return parsed number or {@link Double#NaN} when parsing fails + */ + private static Object parseNumber(String text) { + try { + if (text.contains(DECIMAL_SEPARATOR) + || text.contains(SCIENTIFIC_NOTATION_E_LOWER) + || text.contains(SCIENTIFIC_NOTATION_E_UPPER)) { + return Double.parseDouble(text); + } + return Long.parseLong(text); + } catch (NumberFormatException e) { + return Double.NaN; + } + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbComparisonType.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbComparisonType.java new file mode 100644 index 0000000000..0a719d52d9 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbComparisonType.java @@ -0,0 +1,69 @@ +package org.evomaster.client.java.controller.dynamodb; + +import org.evomaster.client.java.controller.dynamodb.operations.comparison.*; + +/** + * Shared normalized comparison types used by DynamoDB parsers. + */ +public enum DynamoDbComparisonType { + EQUALS, + NOT_EQUALS, + GREATER_THAN, + GREATER_THAN_EQUALS, + LESS_THAN, + LESS_THAN_EQUALS; + + /** + * Maps a DynamoDB comparator token to a normalized comparison type. + * + * @param token comparator token from parsed expression + * @return normalized comparison type + */ + public static DynamoDbComparisonType fromToken(String token) { + if ("=".equals(token)) { + return EQUALS; + } + if ("<>".equals(token)) { + return NOT_EQUALS; + } + if (">".equals(token)) { + return GREATER_THAN; + } + if (">=".equals(token)) { + return GREATER_THAN_EQUALS; + } + if ("<".equals(token)) { + return LESS_THAN; + } + if ("<=".equals(token)) { + return LESS_THAN_EQUALS; + } + throw new IllegalArgumentException("Unsupported comparator token: " + token); + } + + /** + * Creates a comparison operation instance for this comparison type. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param value comparison value + * @return concrete comparison operation + */ + public ComparisonOperation toOperation(String fieldName, Object value) { + switch (this) { + case EQUALS: + return new EqualsOperation<>(fieldName, value); + case NOT_EQUALS: + return new NotEqualsOperation<>(fieldName, value); + case GREATER_THAN: + return new GreaterThanOperation<>(fieldName, value); + case GREATER_THAN_EQUALS: + return new GreaterThanEqualsOperation<>(fieldName, value); + case LESS_THAN: + return new LessThanOperation<>(fieldName, value); + case LESS_THAN_EQUALS: + return new LessThanEqualsOperation<>(fieldName, value); + default: + throw new IllegalStateException("Unsupported comparator: " + this); + } + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbExpressionParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbExpressionParser.java new file mode 100644 index 0000000000..468a0867da --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbExpressionParser.java @@ -0,0 +1,347 @@ +package org.evomaster.client.java.controller.dynamodb; + +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.evomaster.client.java.controller.dynamodb.operations.*; + +import java.util.*; + +/** + * Parser for DynamoDB key/filter/condition expression strings. + * Supported operators/functions align with DynamoDB expression docs: + * =, <>, <, <=, >, >=, BETWEEN, IN, AND, OR, NOT, + * attribute_exists, attribute_not_exists, attribute_type, + * begins_with, contains, size. + */ +public class DynamoDbExpressionParser { + + private Map expressionAttributeNames = Collections.emptyMap(); + private Map expressionAttributeValues = Collections.emptyMap(); + + /** + * Parses a DynamoDB expression and converts it into a query-operation tree. + * + * @param expression the DynamoDB expression string to parse + * @param expressionAttributeNames optional map of attribute-name aliases + * @param expressionAttributeValues optional map of value placeholders + * @return the parsed operation tree, or {@code null} when the expression is blank + */ + public QueryOperation parse( + String expression, + Map expressionAttributeNames, + Map expressionAttributeValues) { + if (expression == null || expression.trim().isEmpty()) { + return null; + } + + this.expressionAttributeNames = expressionAttributeNames == null + ? Collections.emptyMap() + : expressionAttributeNames; + this.expressionAttributeValues = expressionAttributeValues == null + ? Collections.emptyMap() + : expressionAttributeValues; + + try { + DynamoDbConditionExpressionLexer lexer = new DynamoDbConditionExpressionLexer(CharStreams.fromString(expression)); + prepareLexer(lexer); + + CommonTokenStream tokenStream = new CommonTokenStream(lexer); + DynamoDbConditionExpressionParser parser = new DynamoDbConditionExpressionParser(tokenStream); + prepareParser(parser); + + DynamoDbConditionExpressionParser.ExpressionContext tree = parser.expression(); + return new OperationVisitor().visit(tree); + } catch (ParseCancellationException e) { + throw new IllegalArgumentException("Invalid DynamoDB expression: " + expression, e); + } + } + + /** + * Configures lexer error handling to fail fast on invalid input. + * + * @param lexer the lexer to configure + */ + private void prepareLexer(Lexer lexer) { + lexer.removeErrorListeners(); + lexer.addErrorListener(ThrowingErrorListener.INSTANCE); + } + + /** + * Configures parser error handling to fail fast on invalid input. + * + * @param parser the parser to configure + */ + private void prepareParser(Parser parser) { + parser.removeErrorListeners(); + parser.addErrorListener(ThrowingErrorListener.INSTANCE); + } + + /** + * Resolves expression-attribute-name aliases in a dotted field name. + * + * @param token raw field token coming from DynamoDB expression/condition + * @return resolved field name coming from DynamoDB expression/condition + */ + private String parseFieldName(String token) { + String[] chunks = token.split("\\."); + List resolved = new ArrayList<>(chunks.length); + for (String chunk : chunks) { + int bracket = chunk.indexOf('['); + String base = bracket >= 0 ? chunk.substring(0, bracket) : chunk; + String suffix = bracket >= 0 ? chunk.substring(bracket) : ""; + if (base.startsWith("#")) { + resolved.add(expressionAttributeNames.getOrDefault(base, base) + suffix); + } else { + resolved.add(base + suffix); + } + } + return String.join(".", resolved); + } + + /** + * Converts a parsed value node into a runtime Java value. + * + * @param valueContext parsed value context + * @return converted Java value + */ + private Object parseValue(DynamoDbConditionExpressionParser.ValueContext valueContext) { + if (valueContext == null) { + return null; + } + return new ValueVisitor().visit(valueContext); + } + + /** + * Parses a numeric literal into {@link Long} or {@link Double}. + * + * @param token numeric literal token + * @return parsed number, or original token when parsing fails + */ + private Object parseNumberLiteral(String token) { + try { + if (token.contains(".") || token.toLowerCase(Locale.ROOT).contains("e")) { + return Double.parseDouble(token); + } + return Long.parseLong(token); + } catch (NumberFormatException ignored) { + // Keep unknown numeric-like literals as-is. + return token; + } + } + + /** + * Combines two operations with logical AND, flattening nested AND nodes. + * + * @param left left condition + * @param right right condition + * @return merged AND operation + */ + private QueryOperation mergeAnd(QueryOperation left, QueryOperation right) { + if (left instanceof AndOperation) { + List conditions = new ArrayList<>(((AndOperation) left).getConditions()); + conditions.add(right); + return new AndOperation(conditions); + } + return new AndOperation(Arrays.asList(left, right)); + } + + /** + * Combines two operations with logical OR, flattening nested OR nodes. + * + * @param left left condition + * @param right right condition + * @return merged OR operation + */ + private QueryOperation mergeOr(QueryOperation left, QueryOperation right) { + if (left instanceof OrOperation) { + List conditions = new ArrayList<>(((OrOperation) left).getConditions()); + conditions.add(right); + return new OrOperation(conditions); + } + return new OrOperation(Arrays.asList(left, right)); + } + + /** + * Visitor that converts grammar value nodes into Java values. + */ + private class ValueVisitor extends DynamoDbConditionExpressionBaseVisitor { + + /** {@inheritDoc} */ + @Override + public Object visitPlaceholderValue(DynamoDbConditionExpressionParser.PlaceholderValueContext ctx) { + return expressionAttributeValues.get(ctx.PLACEHOLDER().getText()); + } + + /** {@inheritDoc} */ + @Override + public Object visitStringValue(DynamoDbConditionExpressionParser.StringValueContext ctx) { + String token = ctx.STRING_LITERAL().getText(); + return token.substring(1, token.length() - 1); + } + + /** {@inheritDoc} */ + @Override + public Object visitNumberValue(DynamoDbConditionExpressionParser.NumberValueContext ctx) { + return parseNumberLiteral(ctx.NUMBER_LITERAL().getText()); + } + + /** {@inheritDoc} */ + @Override + public Object visitBooleanValue(DynamoDbConditionExpressionParser.BooleanValueContext ctx) { + return "TRUE".equalsIgnoreCase(ctx.BOOLEAN_LITERAL().getText()); + } + + /** {@inheritDoc} */ + @Override + public Object visitNullValue(DynamoDbConditionExpressionParser.NullValueContext ctx) { + return null; + } + + /** {@inheritDoc} */ + @Override + public Object visitIdentifierValue(DynamoDbConditionExpressionParser.IdentifierValueContext ctx) { + return ctx.IDENTIFIER().getText(); + } + } + + /** + * Visitor that converts grammar predicate nodes into query operations. + */ + private class OperationVisitor extends DynamoDbConditionExpressionBaseVisitor { + + /** {@inheritDoc} */ + @Override + public QueryOperation visitExpression(DynamoDbConditionExpressionParser.ExpressionContext ctx) { + return visit(ctx.orExpr()); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitOrExpr(DynamoDbConditionExpressionParser.OrExprContext ctx) { + QueryOperation left = visit(ctx.andExpr(0)); + for (int i = 1; i < ctx.andExpr().size(); i++) { + QueryOperation right = visit(ctx.andExpr(i)); + left = mergeOr(left, right); + } + return left; + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitAndExpr(DynamoDbConditionExpressionParser.AndExprContext ctx) { + QueryOperation left = visit(ctx.notExpr(0)); + for (int i = 1; i < ctx.notExpr().size(); i++) { + QueryOperation right = visit(ctx.notExpr(i)); + left = mergeAnd(left, right); + } + return left; + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitNegatedExpr(DynamoDbConditionExpressionParser.NegatedExprContext ctx) { + return new NotOperation(visit(ctx.notExpr())); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitPrimaryExpr(DynamoDbConditionExpressionParser.PrimaryExprContext ctx) { + return visit(ctx.primary()); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitParenthesizedPrimary(DynamoDbConditionExpressionParser.ParenthesizedPrimaryContext ctx) { + return visit(ctx.orExpr()); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitPredicatePrimary(DynamoDbConditionExpressionParser.PredicatePrimaryContext ctx) { + return visit(ctx.predicate()); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitAttributeExistsPredicate(DynamoDbConditionExpressionParser.AttributeExistsPredicateContext ctx) { + return new ExistsOperation(parseFieldName(ctx.path().getText()), true); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitAttributeNotExistsPredicate(DynamoDbConditionExpressionParser.AttributeNotExistsPredicateContext ctx) { + return new ExistsOperation(parseFieldName(ctx.path().getText()), false); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitAttributeTypePredicate(DynamoDbConditionExpressionParser.AttributeTypePredicateContext ctx) { + Object expectedType = parseValue(ctx.value()); + return new TypeOperation(parseFieldName(ctx.path().getText()), expectedType == null ? null : String.valueOf(expectedType)); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitBeginsWithPredicate(DynamoDbConditionExpressionParser.BeginsWithPredicateContext ctx) { + return new BeginsWithOperation(parseFieldName(ctx.path().getText()), parseValue(ctx.value())); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitContainsPredicate(DynamoDbConditionExpressionParser.ContainsPredicateContext ctx) { + return new ContainsOperation(parseFieldName(ctx.path().getText()), parseValue(ctx.value())); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitSizePredicate(DynamoDbConditionExpressionParser.SizePredicateContext ctx) { + String field = parseFieldName(ctx.path().getText()); + DynamoDbComparisonType comparator = DynamoDbComparisonType.fromToken(ctx.comparator().getText()); + Object expectedValue = parseValue(ctx.value()); + return new SizeOperation(field, comparator, expectedValue); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitBetweenPredicate(DynamoDbConditionExpressionParser.BetweenPredicateContext ctx) { + String field = parseFieldName(ctx.path().getText()); + Object lower = parseValue(ctx.value(0)); + Object upper = parseValue(ctx.value(1)); + return new BetweenOperation(field, lower, upper); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitInPredicate(DynamoDbConditionExpressionParser.InPredicateContext ctx) { + List values = new ArrayList<>(); + for (DynamoDbConditionExpressionParser.ValueContext valueContext : ctx.value()) { + values.add(parseValue(valueContext)); + } + return new InOperation<>(parseFieldName(ctx.path().getText()), values); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitComparisonPredicate(DynamoDbConditionExpressionParser.ComparisonPredicateContext ctx) { + String field = parseFieldName(ctx.path().getText()); + DynamoDbComparisonType comparator = DynamoDbComparisonType.fromToken(ctx.comparator().getText()); + Object value = parseValue(ctx.value()); + return comparator.toOperation(field, value); + } + } + + /** + * see ... + */ + private static class ThrowingErrorListener extends BaseErrorListener { + + private static final ThrowingErrorListener INSTANCE = new ThrowingErrorListener(); + + /** {@inheritDoc} */ + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, + int charPositionInLine, String msg, RecognitionException e) { + throw new ParseCancellationException("line " + line + ":" + charPositionInLine + " " + msg); + } + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbReflectionHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbReflectionHelper.java new file mode 100644 index 0000000000..c042178e00 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbReflectionHelper.java @@ -0,0 +1,46 @@ +package org.evomaster.client.java.controller.dynamodb; + +import java.lang.reflect.Method; + +/** + * Shared reflection helpers used by DynamoDB parser utilities. + */ +public final class DynamoDbReflectionHelper { + + /** + * Utility class, no instances. + */ + private DynamoDbReflectionHelper() { + } + + /** + * Invokes a no-argument method on the target object. + * + * @param target target object + * @param methodName method name to invoke + * @return invocation result or {@code null} on errors + */ + public static Object invokeNoArg(Object target, String methodName) { + if (target == null) { + return null; + } + try { + Method method = target.getClass().getMethod(methodName); + return method.invoke(target); + } catch (Exception ignored) { + return null; + } + } + + /** + * Invokes a no-argument method and returns it only when boolean. + * + * @param target target object + * @param methodName method name to invoke + * @return boolean result or {@code null} + */ + public static Boolean invokeBooleanNoArg(Object target, String methodName) { + Object value = invokeNoArg(target, methodName); + return value instanceof Boolean ? (Boolean) value : null; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbRequestParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbRequestParser.java new file mode 100644 index 0000000000..60d0576d3b --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbRequestParser.java @@ -0,0 +1,71 @@ +package org.evomaster.client.java.controller.dynamodb; + +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; +import org.evomaster.client.java.controller.dynamodb.parsers.*; +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Builds internal operations from DynamoDB request objects. + */ +public class DynamoDbRequestParser { + + private final Map parsersByApiMethod; + + /** + * Creates a request parser and registers supported DynamoDB API method parsers. + */ + public DynamoDbRequestParser() { + Map map = new LinkedHashMap<>(); + registerParser(map, new QueryApiMethodParser()); + registerParser(map, new ScanApiMethodParser()); + registerParser(map, new GetItemApiMethodParser()); + registerParser(map, new BatchGetItemApiMethodParser()); + registerParser(map, new PutItemApiMethodParser()); + registerParser(map, new DeleteItemApiMethodParser()); + registerParser(map, new UpdateItemApiMethodParser()); + this.parsersByApiMethod = Collections.unmodifiableMap(map); + } + + /** + * Entry-point parser used by a future handler. + * It routes a DynamoDB SDK request to the API-method parser and returns + * one parsed condition tree per table name. + * Unsupported operations intentionally yield an empty map. + */ + public Map parseByTable(Object request, DynamoDbOperationNames apiMethodName) { + if (request == null || apiMethodName == null) { + return Collections.emptyMap(); + } + + DynamoDbApiMethodParser parser = parsersByApiMethod.get(apiMethodName); + if (parser == null) { + return Collections.emptyMap(); + } + + Map parsed = parser.parseRequest(request); + return parsed == null ? Collections.emptyMap() : parsed; + } + + /** + * Registers one API-method parser and rejects duplicates. + * + * @param parsersByApiMethod parser registry by API method name + * @param parser parser instance to register + */ + private static void registerParser(Map parsersByApiMethod, + DynamoDbApiMethodParser parser) { + DynamoDbOperationNames apiMethodName = parser.apiMethodName(); + if (apiMethodName == null) { + throw new IllegalArgumentException("Parser api method name cannot be null or blank"); + } + + DynamoDbApiMethodParser previous = parsersByApiMethod.put(apiMethodName, parser); + if (previous != null) { + throw new IllegalStateException("Duplicate parser for api method " + apiMethodName); + } + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/AndOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/AndOperation.java new file mode 100644 index 0000000000..00a29020ac --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/AndOperation.java @@ -0,0 +1,29 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +import java.util.List; + +/** + * Logical AND operation over multiple DynamoDB query conditions. + */ +public class AndOperation extends QueryOperation { + + private final List conditions; + + /** + * Creates an AND operation. + * + * @param conditions conditions to combine + */ + public AndOperation(List conditions) { + this.conditions = conditions; + } + + /** + * Returns the conditions combined by this AND operation. + * + * @return combined conditions + */ + public List getConditions() { + return conditions; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/BeginsWithOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/BeginsWithOperation.java new file mode 100644 index 0000000000..5e5bc8ce22 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/BeginsWithOperation.java @@ -0,0 +1,35 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +/** + * DynamoDB {@code begins_with(path, value)} predicate operation. + */ +public class BeginsWithOperation extends QueryOperation { + + private final String fieldName; + private final Object prefix; + + /** + * Creates a begins-with operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param prefix expected prefix + */ + public BeginsWithOperation(String fieldName, Object prefix) { + this.fieldName = fieldName; + this.prefix = prefix; + } + + /** + * @return field name coming from DynamoDB expression/condition + */ + public String getFieldName() { + return fieldName; + } + + /** + * @return expected prefix + */ + public Object getPrefix() { + return prefix; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/BetweenOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/BetweenOperation.java new file mode 100644 index 0000000000..3e60a9c4ed --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/BetweenOperation.java @@ -0,0 +1,45 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +/** + * DynamoDB {@code path BETWEEN lower AND upper} predicate operation. + */ +public class BetweenOperation extends QueryOperation { + + private final String fieldName; + private final Object lowerBound; + private final Object upperBound; + + /** + * Creates a BETWEEN operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param lowerBound lower bound value + * @param upperBound upper bound value + */ + public BetweenOperation(String fieldName, Object lowerBound, Object upperBound) { + this.fieldName = fieldName; + this.lowerBound = lowerBound; + this.upperBound = upperBound; + } + + /** + * @return field name coming from DynamoDB expression/condition + */ + public String getFieldName() { + return fieldName; + } + + /** + * @return lower bound value + */ + public Object getLowerBound() { + return lowerBound; + } + + /** + * @return upper bound value + */ + public Object getUpperBound() { + return upperBound; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/ContainsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/ContainsOperation.java new file mode 100644 index 0000000000..9580d026c2 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/ContainsOperation.java @@ -0,0 +1,35 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +/** + * DynamoDB {@code contains(path, value)} predicate operation. + */ +public class ContainsOperation extends QueryOperation { + + private final String fieldName; + private final Object expectedValue; + + /** + * Creates a contains operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param expectedValue expected contained value + */ + public ContainsOperation(String fieldName, Object expectedValue) { + this.fieldName = fieldName; + this.expectedValue = expectedValue; + } + + /** + * @return field name coming from DynamoDB expression/condition + */ + public String getFieldName() { + return fieldName; + } + + /** + * @return expected contained value + */ + public Object getExpectedValue() { + return expectedValue; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/ExistsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/ExistsOperation.java new file mode 100644 index 0000000000..e45cc3e333 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/ExistsOperation.java @@ -0,0 +1,37 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +/** + * DynamoDB existence predicate operation for {@code attribute_exists} and + * {@code attribute_not_exists}. + */ +public class ExistsOperation extends QueryOperation { + + private final String fieldName; + //true = exists, false = not exists + private final boolean exists; + + /** + * Creates an existence operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param exists {@code true} for exists, {@code false} for not-exists + */ + public ExistsOperation(String fieldName, boolean exists) { + this.fieldName = fieldName; + this.exists = exists; + } + + /** + * @return field name coming from DynamoDB expression/condition + */ + public String getFieldName() { + return fieldName; + } + + /** + * @return {@code true} when existence is required, {@code false} otherwise + */ + public boolean isExists() { + return exists; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/InOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/InOperation.java new file mode 100644 index 0000000000..df230f0fd9 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/InOperation.java @@ -0,0 +1,39 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +import java.util.List; + +/** + * DynamoDB {@code path IN (...)} predicate operation. + * + * @param value type + */ +public class InOperation extends QueryOperation { + + private final String fieldName; + private final List values; + + /** + * Creates an IN operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param values candidate values + */ + public InOperation(String fieldName, List values) { + this.fieldName = fieldName; + this.values = values; + } + + /** + * @return field name coming from DynamoDB expression/condition + */ + public String getFieldName() { + return fieldName; + } + + /** + * @return candidate values + */ + public List getValues() { + return values; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/NotOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/NotOperation.java new file mode 100644 index 0000000000..3be8bc95dd --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/NotOperation.java @@ -0,0 +1,25 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +/** + * Logical NOT operation over one DynamoDB query condition. + */ +public class NotOperation extends QueryOperation { + + private final QueryOperation condition; + + /** + * Creates a NOT operation. + * + * @param condition condition to negate + */ + public NotOperation(QueryOperation condition) { + this.condition = condition; + } + + /** + * @return negated condition + */ + public QueryOperation getCondition() { + return condition; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/OrOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/OrOperation.java new file mode 100644 index 0000000000..0f9eb05460 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/OrOperation.java @@ -0,0 +1,29 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +import java.util.List; + +/** + * Logical OR operation over multiple DynamoDB query conditions. + */ +public class OrOperation extends QueryOperation { + + private final List conditions; + + /** + * Creates an OR operation. + * + * @param conditions conditions to combine + */ + public OrOperation(List conditions) { + this.conditions = conditions; + } + + /** + * Returns the conditions combined by this OR operation. + * + * @return combined conditions + */ + public List getConditions() { + return conditions; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/QueryOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/QueryOperation.java new file mode 100644 index 0000000000..3748bb1f16 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/QueryOperation.java @@ -0,0 +1,7 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +/** + * Represents a DynamoDB condition/filter expression operation. + */ +public abstract class QueryOperation { +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/SizeOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/SizeOperation.java new file mode 100644 index 0000000000..eac67f0c1c --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/SizeOperation.java @@ -0,0 +1,47 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +import org.evomaster.client.java.controller.dynamodb.DynamoDbComparisonType; + +/** + * DynamoDB {@code size(path) comparator value} predicate operation. + */ +public class SizeOperation extends QueryOperation { + + private final String fieldName; + private final DynamoDbComparisonType comparator; + private final Object expectedValue; + + /** + * Creates a size operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param comparator comparison operator + * @param expectedValue expected value + */ + public SizeOperation(String fieldName, DynamoDbComparisonType comparator, Object expectedValue) { + this.fieldName = fieldName; + this.comparator = comparator; + this.expectedValue = expectedValue; + } + + /** + * @return field name coming from DynamoDB expression/condition + */ + public String getFieldName() { + return fieldName; + } + + /** + * @return comparison operator + */ + public DynamoDbComparisonType getComparator() { + return comparator; + } + + /** + * @return expected value + */ + public Object getExpectedValue() { + return expectedValue; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/TypeOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/TypeOperation.java new file mode 100644 index 0000000000..8dbf35e230 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/TypeOperation.java @@ -0,0 +1,35 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +/** + * DynamoDB {@code attribute_type(path, type)} predicate operation. + */ +public class TypeOperation extends QueryOperation { + + private final String fieldName; + private final String expectedType; + + /** + * Creates a type operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param expectedType expected DynamoDB type token + */ + public TypeOperation(String fieldName, String expectedType) { + this.fieldName = fieldName; + this.expectedType = expectedType; + } + + /** + * @return field name coming from DynamoDB expression/condition + */ + public String getFieldName() { + return fieldName; + } + + /** + * @return expected DynamoDB type token + */ + public String getExpectedType() { + return expectedType; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/ComparisonOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/ComparisonOperation.java new file mode 100644 index 0000000000..67aa85090e --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/ComparisonOperation.java @@ -0,0 +1,39 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; + +/** + * Base class for comparison operations over a field and value. + * + * @param value type + */ +public abstract class ComparisonOperation extends QueryOperation { + + private final String fieldName; + private final V value; + + /** + * Creates a comparison operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param value comparison value + */ + ComparisonOperation(String fieldName, V value) { + this.fieldName = fieldName; + this.value = value; + } + + /** + * @return field name coming from DynamoDB expression/condition + */ + public String getFieldName() { + return fieldName; + } + + /** + * @return comparison value + */ + public V getValue() { + return value; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/EqualsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/EqualsOperation.java new file mode 100644 index 0000000000..8fe7b06036 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/EqualsOperation.java @@ -0,0 +1,19 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +/** + * Equality comparison operation ({@code =}). + * + * @param value type + */ +public class EqualsOperation extends ComparisonOperation { + + /** + * Creates an equality comparison operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param value comparison value + */ + public EqualsOperation(String fieldName, V value) { + super(fieldName, value); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/GreaterThanEqualsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/GreaterThanEqualsOperation.java new file mode 100644 index 0000000000..c042f63f10 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/GreaterThanEqualsOperation.java @@ -0,0 +1,19 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +/** + * Greater-than-or-equals comparison operation ({@code >=}). + * + * @param value type + */ +public class GreaterThanEqualsOperation extends ComparisonOperation { + + /** + * Creates a greater-than-or-equals comparison operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param value comparison value + */ + public GreaterThanEqualsOperation(String fieldName, V value) { + super(fieldName, value); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/GreaterThanOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/GreaterThanOperation.java new file mode 100644 index 0000000000..6ccdf43572 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/GreaterThanOperation.java @@ -0,0 +1,19 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +/** + * Greater-than comparison operation ({@code >}). + * + * @param value type + */ +public class GreaterThanOperation extends ComparisonOperation { + + /** + * Creates a greater-than comparison operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param value comparison value + */ + public GreaterThanOperation(String fieldName, V value) { + super(fieldName, value); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/LessThanEqualsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/LessThanEqualsOperation.java new file mode 100644 index 0000000000..9ca900ae1d --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/LessThanEqualsOperation.java @@ -0,0 +1,19 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +/** + * Less-than-or-equals comparison operation ({@code <=}). + * + * @param value type + */ +public class LessThanEqualsOperation extends ComparisonOperation { + + /** + * Creates a less-than-or-equals comparison operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param value comparison value + */ + public LessThanEqualsOperation(String fieldName, V value) { + super(fieldName, value); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/LessThanOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/LessThanOperation.java new file mode 100644 index 0000000000..8288cbb170 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/LessThanOperation.java @@ -0,0 +1,19 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +/** + * Less-than comparison operation ({@code <}). + * + * @param value type + */ +public class LessThanOperation extends ComparisonOperation { + + /** + * Creates a less-than comparison operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param value comparison value + */ + public LessThanOperation(String fieldName, V value) { + super(fieldName, value); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/NotEqualsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/NotEqualsOperation.java new file mode 100644 index 0000000000..42eb51e4cb --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/NotEqualsOperation.java @@ -0,0 +1,19 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +/** + * Inequality comparison operation ({@code <>}). + * + * @param value type + */ +public class NotEqualsOperation extends ComparisonOperation { + + /** + * Creates an inequality comparison operation. + * + * @param fieldName field name coming from DynamoDB expression/condition + * @param value comparison value + */ + public NotEqualsOperation(String fieldName, V value) { + super(fieldName, value); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/BatchGetItemApiMethodParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/BatchGetItemApiMethodParser.java new file mode 100644 index 0000000000..70b12cbd3a --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/BatchGetItemApiMethodParser.java @@ -0,0 +1,76 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.controller.dynamodb.DynamoDbAttributeValueHelper; +import org.evomaster.client.java.controller.dynamodb.operations.OrOperation; +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +import java.util.*; + +import static org.evomaster.client.java.controller.dynamodb.DynamoDbReflectionHelper.invokeNoArg; + +/** + * Parser for DynamoDB {@code BatchGetItem} requests. + */ +public class BatchGetItemApiMethodParser extends DynamoDbBaseApiMethodParser { + + /** + * {@inheritDoc} + */ + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.BATCH_GET_ITEM; + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public Map parseRequest(Object request) { + Object requestItemsObj = invokeNoArg(request, METHOD_REQUEST_ITEMS); + if (!(requestItemsObj instanceof Map)) { + return Collections.emptyMap(); + } + + Map result = new LinkedHashMap<>(); + Map requestItems = (Map) requestItemsObj; + for (Map.Entry entry : requestItems.entrySet()) { + String tableName = entry.getKey() == null ? null : String.valueOf(entry.getKey()); + if (tableName == null || tableName.trim().isEmpty()) { + continue; + } + + Object keysAndAttributes = entry.getValue(); + Object keysObj = invokeNoArg(keysAndAttributes, METHOD_KEYS); + if (!(keysObj instanceof Collection)) { + continue; + } + + List keyConditions = new ArrayList<>(); + for (Object rawKey : (Collection) keysObj) { + QueryOperation keyCondition = buildEqualsFromMap(DynamoDbAttributeValueHelper.toPlainMap(rawKey)); + if (keyCondition != null) { + keyConditions.add(keyCondition); + } + } + + QueryOperation tableOperation = combineWithOr(keyConditions); + if (tableOperation != null) { + result.put(tableName, tableOperation); + } + } + + return result; + } + + /** + * Combines key conditions with OR semantics. + * + * @param conditions per-key conditions + * @return combined operation + */ + private QueryOperation combineWithOr(List conditions) { + return combine(conditions, OrOperation::new); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DeleteItemApiMethodParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DeleteItemApiMethodParser.java new file mode 100644 index 0000000000..63e8a553ce --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DeleteItemApiMethodParser.java @@ -0,0 +1,25 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +/** + * Parser for DynamoDB {@code DeleteItem} requests. + */ +public class DeleteItemApiMethodParser extends WriteMethodParser { + + /** + * {@inheritDoc} + */ + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.DELETE_ITEM; + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean requiresKeyCondition() { + return true; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DynamoDbApiMethodParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DynamoDbApiMethodParser.java new file mode 100644 index 0000000000..be18a58124 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DynamoDbApiMethodParser.java @@ -0,0 +1,27 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +import java.util.Map; + +/** + * Parser for one DynamoDB API method family. + */ +public interface DynamoDbApiMethodParser { + + /** + * Returns the DynamoDB API method handled by this parser. + * + * @return API method identifier + */ + DynamoDbOperationNames apiMethodName(); + + /** + * Parses one request object into table-specific query operations. + * + * @param request DynamoDB request object + * @return a map of parsed operations by table name + */ + Map parseRequest(Object request); +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DynamoDbBaseApiMethodParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DynamoDbBaseApiMethodParser.java new file mode 100644 index 0000000000..9637148444 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DynamoDbBaseApiMethodParser.java @@ -0,0 +1,191 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.controller.dynamodb.DynamoDbAttributeValueHelper; +import org.evomaster.client.java.controller.dynamodb.DynamoDbExpressionParser; +import org.evomaster.client.java.controller.dynamodb.operations.AndOperation; +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; +import org.evomaster.client.java.controller.dynamodb.operations.comparison.EqualsOperation; + +import java.util.*; +import java.util.function.Function; + +import static org.evomaster.client.java.controller.dynamodb.DynamoDbReflectionHelper.invokeNoArg; + +/** + * Base class for DynamoDB SDK requests parser. Contains shared utilities. + */ +abstract class DynamoDbBaseApiMethodParser implements DynamoDbApiMethodParser { + + // All these constants are used to invoke DDB API methods by reflection, do not change. + protected static final String METHOD_TABLE_NAME = "tableName"; + protected static final String METHOD_KEY_CONDITION_EXPRESSION = "keyConditionExpression"; + protected static final String METHOD_FILTER_EXPRESSION = "filterExpression"; + protected static final String METHOD_KEY = "key"; + protected static final String METHOD_REQUEST_ITEMS = "requestItems"; + protected static final String METHOD_KEYS = "keys"; + protected static final String METHOD_CONDITION_EXPRESSION = "conditionExpression"; + protected static final String METHOD_EXPRESSION_ATTRIBUTE_NAMES = "expressionAttributeNames"; + protected static final String METHOD_EXPRESSION_ATTRIBUTE_VALUES = "expressionAttributeValues"; + + /** + * Parses a DynamoDB expression string into a query operation. + * + * @param expression expression string + * @param expressionAttributeNames name placeholders map + * @param expressionAttributeValues value placeholders map + * @return parsed operation, or {@code null} + */ + protected QueryOperation parseExpression( + String expression, + Map expressionAttributeNames, + Map expressionAttributeValues) { + return new DynamoDbExpressionParser().parse(expression, expressionAttributeNames, expressionAttributeValues); + } + + /** + * Parses key equality conditions from request key fields. + * + * @param request request object + * @return parsed key condition operation + */ + protected QueryOperation parseKeyCondition(Object request) { + Object keyObj = invokeNoArg(request, METHOD_KEY); + return buildEqualsFromMap(DynamoDbAttributeValueHelper.toPlainMap(keyObj)); + } + + /** + * Builds equality operations from a field/value map. + * + * @param values field/value map + * @return combined equality operation, or {@code null} + */ + protected QueryOperation buildEqualsFromMap(Map values) { + if (values == null || values.isEmpty()) { + return null; + } + + List conditions = new ArrayList<>(); + values.forEach((key, value) -> conditions.add(new EqualsOperation<>(key, value))); + return combineWithAnd(conditions); + } + + /** + * Combines two operations with AND semantics. + * + * @param left left operation + * @param right right operation + * @return combined operation + */ + protected QueryOperation combineWithAnd(QueryOperation left, QueryOperation right) { + return combineWithAnd(Arrays.asList(left, right)); + } + + /** + * Combines a list of operations with AND semantics. + * + * @param conditions conditions to combine + * @return combined operation + */ + protected QueryOperation combineWithAnd(List conditions) { + return combine(conditions, AndOperation::new); + } + + /** + * Combines a list of operations with a provided composite builder, skipping null entries. + * + * @param conditions operations to combine + * @param compositeBuilder builder for composite operation + * @return combined operation, one operation, or {@code null} + */ + protected QueryOperation combine(List conditions, Function, QueryOperation> compositeBuilder) { + List filtered = new ArrayList<>(); + for (QueryOperation operation : conditions) { + if (operation != null) { + filtered.add(operation); + } + } + + if (filtered.isEmpty()) { + return null; + } + if (filtered.size() == 1) { + return filtered.get(0); + } + return compositeBuilder.apply(filtered); + } + + /** + * Reads and normalizes expression attribute names from request object. + * + * @param request request object + * @return normalized name map + */ + protected Map readNameMap(Object request) { + Object raw = invokeNoArg(request, METHOD_EXPRESSION_ATTRIBUTE_NAMES); + if (!(raw instanceof Map)) { + return Collections.emptyMap(); + } + + Map result = new LinkedHashMap<>(); + ((Map) raw).forEach((k, v) -> { + if (k != null && v != null) { + result.put(String.valueOf(k), String.valueOf(v)); + } + }); + return result; + } + + /** + * Reads and converts expression attribute values from request object. + * + * @param request request object + * @return normalized value map + */ + protected Map readValueMap(Object request) { + Object raw = invokeNoArg(request, METHOD_EXPRESSION_ATTRIBUTE_VALUES); + return DynamoDbAttributeValueHelper.toPlainMap(raw); + } + + /** + * Reads a string-like value from request object via reflection. + * + * @param target target object + * @param methodName accessor method name + * @return string value, or {@code null} + */ + protected String readString(Object target, String methodName) { + Object value = invokeNoArg(target, methodName); + return value == null ? null : String.valueOf(value); + } + + /** + * Reads and validates the table name from the request. + * + * @param request request object + * @return table name, or {@code null} if blank/absent + */ + protected String readValidTableName(Object request) { + String tableName = readString(request, METHOD_TABLE_NAME); + if (tableName == null || tableName.trim().isEmpty()) { + return null; + } + return tableName; + } + + /** + * Builds a singleton result map for one table/operation pair. + * + * @param tableName table name + * @param operation parsed operation + * @return singleton map or empty map when invalid + */ + protected Map singleTableResult(String tableName, QueryOperation operation) { + if (tableName == null || operation == null) { + return Collections.emptyMap(); + } + + Map result = new LinkedHashMap<>(); + result.put(tableName, operation); + return result; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/GetItemApiMethodParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/GetItemApiMethodParser.java new file mode 100644 index 0000000000..e6dac06531 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/GetItemApiMethodParser.java @@ -0,0 +1,30 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +import java.util.Map; + +/** + * Parser for DynamoDB {@code GetItem} requests. + */ +public class GetItemApiMethodParser extends DynamoDbBaseApiMethodParser { + + /** + * {@inheritDoc} + */ + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.GET_ITEM; + } + + /** + * {@inheritDoc} + */ + @Override + public Map parseRequest(Object request) { + String tableName = readValidTableName(request); + QueryOperation keyCondition = parseKeyCondition(request); + return singleTableResult(tableName, keyCondition); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/PutItemApiMethodParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/PutItemApiMethodParser.java new file mode 100644 index 0000000000..35619e3c72 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/PutItemApiMethodParser.java @@ -0,0 +1,25 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +/** + * Parser for DynamoDB {@code PutItem} requests. + */ +public class PutItemApiMethodParser extends WriteMethodParser { + + /** + * {@inheritDoc} + */ + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.PUT_ITEM; + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean requiresKeyCondition() { + return false; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/QueryApiMethodParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/QueryApiMethodParser.java new file mode 100644 index 0000000000..2003448fe9 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/QueryApiMethodParser.java @@ -0,0 +1,48 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +import java.util.Collections; +import java.util.Map; + +/** + * Parser for DynamoDB {@code Query} requests. + */ +public class QueryApiMethodParser extends DynamoDbBaseApiMethodParser { + + /** + * {@inheritDoc} + */ + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.QUERY; + } + + /** + * {@inheritDoc} + */ + @Override + public Map parseRequest(Object request) { + String tableName = readValidTableName(request); + if (tableName == null) { + return Collections.emptyMap(); + } + + Map names = readNameMap(request); + Map values = readValueMap(request); + + QueryOperation keyCondition = parseExpression( + readString(request, METHOD_KEY_CONDITION_EXPRESSION), + names, + values + ); + QueryOperation filterCondition = parseExpression( + readString(request, METHOD_FILTER_EXPRESSION), + names, + values + ); + + return singleTableResult(tableName, combineWithAnd(keyCondition, filterCondition)); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/ScanApiMethodParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/ScanApiMethodParser.java new file mode 100644 index 0000000000..9da73906b0 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/ScanApiMethodParser.java @@ -0,0 +1,40 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +import java.util.Collections; +import java.util.Map; + +/** + * Parser for DynamoDB {@code Scan} requests. + */ +public class ScanApiMethodParser extends DynamoDbBaseApiMethodParser { + + /** + * {@inheritDoc} + */ + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.SCAN; + } + + /** + * {@inheritDoc} + */ + @Override + public Map parseRequest(Object request) { + String tableName = readValidTableName(request); + if (tableName == null) { + return Collections.emptyMap(); + } + + QueryOperation filterCondition = parseExpression( + readString(request, METHOD_FILTER_EXPRESSION), + readNameMap(request), + readValueMap(request) + ); + + return singleTableResult(tableName, filterCondition); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/UpdateItemApiMethodParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/UpdateItemApiMethodParser.java new file mode 100644 index 0000000000..e215541607 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/UpdateItemApiMethodParser.java @@ -0,0 +1,25 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +/** + * Parser for DynamoDB {@code UpdateItem} requests. + */ +public class UpdateItemApiMethodParser extends WriteMethodParser { + + /** + * {@inheritDoc} + */ + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.UPDATE_ITEM; + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean requiresKeyCondition() { + return true; + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/WriteMethodParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/WriteMethodParser.java new file mode 100644 index 0000000000..49bb00422d --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/WriteMethodParser.java @@ -0,0 +1,39 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; + +import java.util.Collections; +import java.util.Map; + +/** + * Base parser for write APIs with optional key and condition expressions. + */ +abstract class WriteMethodParser extends DynamoDbBaseApiMethodParser { + + /** + * Indicates whether the write method requires key conditions. + * + * @return {@code true} if key conditions are required + */ + protected abstract boolean requiresKeyCondition(); + + /** + * {@inheritDoc} + */ + @Override + public final Map parseRequest(Object request) { + String tableName = readValidTableName(request); + if (tableName == null) { + return Collections.emptyMap(); + } + + QueryOperation keyCondition = requiresKeyCondition() ? parseKeyCondition(request) : null; + QueryOperation conditionExpression = parseExpression( + readString(request, METHOD_CONDITION_EXPRESSION), + readNameMap(request), + readValueMap(request) + ); + + return singleTableResult(tableName, combineWithAnd(keyCondition, conditionExpression)); + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbAttributeValueHelperTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbAttributeValueHelperTest.java new file mode 100644 index 0000000000..c0aa64b443 --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbAttributeValueHelperTest.java @@ -0,0 +1,183 @@ +package org.evomaster.client.java.controller.dynamodb; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.nio.ByteBuffer; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class DynamoDbAttributeValueHelperTest { + + @Test + public void testToPlainMapWithNonMapReturnsEmpty() { + assertTrue(DynamoDbAttributeValueHelper.toPlainMap("world-cup").isEmpty()); + } + + @Test + public void testToPlainMapConvertsKeysAndSkipsNullKeys() { + Map source = new LinkedHashMap<>(); + source.put(10, AttributeValue.builder().s("Lionel Messi").build()); + source.put(null, AttributeValue.builder().s("Kylian Mbappe").build()); + + Map plain = DynamoDbAttributeValueHelper.toPlainMap(source); + + assertEquals(1, plain.size()); + assertEquals("Lionel Messi", plain.get("10")); + } + + @Test + public void testToPlainValueForNullMapAndCollection() { + assertNull(DynamoDbAttributeValueHelper.toPlainValue(null)); + + Map map = new LinkedHashMap<>(); + map.put("goals", AttributeValue.builder().n("7").build()); + assertEquals(7L, ((Map) DynamoDbAttributeValueHelper.toPlainValue(map)).get("goals")); + + List list = Arrays.asList( + AttributeValue.builder().s("Argentina").build(), + AttributeValue.builder().bool(true).build() + ); + assertEquals(Arrays.asList("Argentina", true), DynamoDbAttributeValueHelper.toPlainValue(list)); + } + + @Test + public void testToPlainValueWithNulHasPriority() { + AttributeValue value = AttributeValue.builder().nul(true).s("Messi").n("36").bool(true).build(); + assertNull(DynamoDbAttributeValueHelper.toPlainValue(value)); + } + + @Test + public void testToPlainValueWithNumberParsingVariants() { + assertEquals(13L, DynamoDbAttributeValueHelper.toPlainValue(AttributeValue.builder().n("13").build())); + assertEquals(1.75, (Double) DynamoDbAttributeValueHelper.toPlainValue(AttributeValue.builder().n("1.75").build()), 0.000001); + assertEquals(30.0, (Double) DynamoDbAttributeValueHelper.toPlainValue(AttributeValue.builder().n("3e1").build()), 0.000001); + + Object invalid = DynamoDbAttributeValueHelper.toPlainValue(AttributeValue.builder().n("goals").build()); + assertInstanceOf(Double.class, invalid); + assertTrue(Double.isNaN((Double) invalid)); + } + + @Test + public void testToPlainValueWithEmptyNumberFallsBackToBool() { + Object value = DynamoDbAttributeValueHelper.toPlainValue(new FakeAttributeValue("", true)); + assertEquals(true, value); + } + + @Test + public void testToPlainValueWithMapListAndSetShapes() { + Map nested = new LinkedHashMap<>(); + nested.put("player", AttributeValue.builder().s("Mbappe").build()); + + assertEquals(Collections.singletonMap("player", "Mbappe"), + DynamoDbAttributeValueHelper.toPlainValue(AttributeValue.builder().m(nested).build())); + + List list = Arrays.asList( + AttributeValue.builder().s("France").build(), + AttributeValue.builder().n("8").build() + ); + assertEquals(Arrays.asList("France", 8L), + DynamoDbAttributeValueHelper.toPlainValue(AttributeValue.builder().l(list).build())); + + Object ss = DynamoDbAttributeValueHelper.toPlainValue(AttributeValue.builder().ss("Argentina", "Argentina", "France").build()); + assertEquals(new LinkedHashSet<>(Arrays.asList("Argentina", "France")), ss); + + Object ns = DynamoDbAttributeValueHelper.toPlainValue(AttributeValue.builder().ns("36", "7.5", "age?").build()); + assertInstanceOf(Set.class, ns); + assertEquals(3, ((Set) ns).size()); + assertTrue(((Set) ns).contains(36L)); + assertTrue(((Set) ns).contains(7.5)); + assertTrue(((Set) ns).stream().anyMatch(v -> v instanceof Double && Double.isNaN((Double) v))); + } + + @Test + public void testToPlainValueWithBinaryAndBinarySet() { + SdkBytes binary = SdkBytes.fromByteArray(new byte[]{1, 2, 3}); + Object single = DynamoDbAttributeValueHelper.toPlainValue(AttributeValue.builder().b(binary).build()); + assertEquals(binary, single); + + SdkBytes bsBinary = SdkBytes.fromByteArray(new byte[]{7, 8}); + Object bs = DynamoDbAttributeValueHelper.toPlainValue(AttributeValue.builder().bs(bsBinary).build()); + assertInstanceOf(Set.class, bs); + assertEquals(1, ((Set) bs).size()); + assertEquals(bsBinary, ((Set) bs).iterator().next()); + } + + @Test + public void testToPlainValueWithDirectByteBufferBinary() { + Object converted = DynamoDbAttributeValueHelper.toPlainValue(new FakeBinaryAttributeValue(ByteBuffer.wrap(new byte[]{4, 5}))); + assertArrayEquals(new byte[]{4, 5}, (byte[]) converted); + } + + @Test + public void testToPlainValueWithBinarySetContainingNonBinary() { + Object converted = DynamoDbAttributeValueHelper.toPlainValue( + new FakeBinarySetAttributeValue(Arrays.asList(ByteBuffer.wrap(new byte[]{9}), "Brazil")) + ); + + assertInstanceOf(Set.class, converted); + assertEquals(2, ((Set) converted).size()); + Iterator it = ((Set) converted).iterator(); + assertArrayEquals(new byte[]{9}, (byte[]) it.next()); + assertEquals("Brazil", it.next()); + } + + @Test + public void testToPlainValueFallbackWhenNoKnownShape() { + Object marker = new Object(); + assertSame(marker, DynamoDbAttributeValueHelper.toPlainValue(marker)); + } + + private static class FakeAttributeValue { + private final String n; + private final Boolean bool; + + private FakeAttributeValue(String n, Boolean bool) { + this.n = n; + this.bool = bool; + } + + @SuppressWarnings("unused") + public String n() { + return n; + } + + @SuppressWarnings("unused") + public Boolean bool() { + return bool; + } + } + + private static class FakeBinarySetAttributeValue { + private final Collection bs; + + private FakeBinarySetAttributeValue(Collection bs) { + this.bs = bs; + } + + @SuppressWarnings("unused") + public Boolean hasBs() { + return true; + } + + @SuppressWarnings("unused") + public Collection bs() { + return bs; + } + } + + private static class FakeBinaryAttributeValue { + private final Object b; + + private FakeBinaryAttributeValue(Object b) { + this.b = b; + } + + @SuppressWarnings("unused") + public Object b() { + return b; + } + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbExpressionParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbExpressionParserTest.java new file mode 100644 index 0000000000..f9149e7fc1 --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbExpressionParserTest.java @@ -0,0 +1,123 @@ +package org.evomaster.client.java.controller.dynamodb; + +import org.evomaster.client.java.controller.dynamodb.operations.*; +import org.evomaster.client.java.controller.dynamodb.operations.comparison.*; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +public class DynamoDbExpressionParserTest extends DynamoDbTestBase { + + private final DynamoDbExpressionParser parser = new DynamoDbExpressionParser(); + + @Test + public void testBlankAndInvalidExpressions() { + assertNull(parser.parse(null, Collections.emptyMap(), Collections.emptyMap())); + assertNull(parser.parse(" ", Collections.emptyMap(), Collections.emptyMap())); + assertThrows(IllegalArgumentException.class, + () -> parser.parse("id =", Collections.emptyMap(), Collections.emptyMap())); + } + + @Test + public void testComparisonOperatorsAndValueKinds() { + assertComparison(parser.parse("playerName = 'Messi'", null, null), + EqualsOperation.class, "playerName", "Messi"); + + assertComparison(parser.parse("worldCups <> 3", null, null), + NotEqualsOperation.class, "worldCups", 3L); + + ComparisonOperation gt = castAs(parser.parse("internationalCaps > 1.5e2", null, null), GreaterThanOperation.class); + assertEquals("internationalCaps", gt.getFieldName()); + assertInstanceOf(Double.class, gt.getValue()); + assertEquals(150.0, (Double) gt.getValue(), 0.000001); + + assertComparison( + parser.parse("age >= :v", null, values(":v", 38L)), + GreaterThanEqualsOperation.class, "age", 38L); + + assertComparison(parser.parse("retired < FALSE", null, null), + LessThanOperation.class, "retired", false); + + assertComparison(parser.parse("nickname <= NULL", null, null), + LessThanEqualsOperation.class, "nickname", null); + } + + @Test + public void testFunctionsLogicalCompositionAliasesAndIndexes() { + QueryOperation operation = parser.parse( + "NOT (attribute_exists(#a[0].#b) AND begins_with(email, :p) AND contains(titles, 'World Cup')) " + + "OR attribute_type(legendType, S) " + + "OR size(teams) >= 2", + names("#a", "squads", "#b", "captain"), + values(":p", "messi@") + ); + + OrOperation rootOr = castAs(operation, OrOperation.class); + assertEquals(3, rootOr.getConditions().size()); + + NotOperation not = castAs(rootOr.getConditions().get(0), NotOperation.class); + AndOperation and = castAs(not.getCondition(), AndOperation.class); + assertEquals(3, and.getConditions().size()); + + ExistsOperation exists = castAs(and.getConditions().get(0), ExistsOperation.class); + assertEquals("squads[0].captain", exists.getFieldName()); + assertTrue(exists.isExists()); + + BeginsWithOperation beginsWith = castAs(and.getConditions().get(1), BeginsWithOperation.class); + assertEquals("email", beginsWith.getFieldName()); + assertEquals("messi@", beginsWith.getPrefix()); + + ContainsOperation contains = castAs(and.getConditions().get(2), ContainsOperation.class); + assertEquals("titles", contains.getFieldName()); + assertEquals("World Cup", contains.getExpectedValue()); + + TypeOperation type = castAs(rootOr.getConditions().get(1), TypeOperation.class); + assertEquals("legendType", type.getFieldName()); + assertEquals("S", type.getExpectedType()); + + SizeOperation size = castAs(rootOr.getConditions().get(2), SizeOperation.class); + assertEquals("teams", size.getFieldName()); + assertEquals(DynamoDbComparisonType.GREATER_THAN_EQUALS, size.getComparator()); + assertEquals(2L, size.getExpectedValue()); + } + + @Test + public void testBetweenAndInWithMixedValues() { + QueryOperation operation = parser.parse( + "age BETWEEN :low AND 41 AND playerName IN (:s1, 'Maradona', Pele)", + null, + values(":low", 38L, ":s1", "Messi") + ); + + AndOperation and = castAs(operation, AndOperation.class); + assertEquals(2, and.getConditions().size()); + + BetweenOperation between = castAs(and.getConditions().get(0), BetweenOperation.class); + assertEquals("age", between.getFieldName()); + assertEquals(38L, between.getLowerBound()); + assertEquals(41L, between.getUpperBound()); + + InOperation in = castAs(and.getConditions().get(1), InOperation.class); + assertEquals("playerName", in.getFieldName()); + assertEquals(Arrays.asList("Messi", "Maradona", "Pele"), in.getValues()); + } + + @Test + public void testFallbacksForMissingPlaceholderAliasAndOverflowNumberLiteral() { + assertComparison( + parser.parse("#playerId = :missing", Collections.emptyMap(), Collections.emptyMap()), + EqualsOperation.class, + "#playerId", + null + ); + + ComparisonOperation comparison = castAs(parser.parse("allTimeGoals = 9999999999999999999999999999999999999", + null, null), EqualsOperation.class); + assertEquals("allTimeGoals", comparison.getFieldName()); + //Assert the number was parsed as a String as a fallback + assertEquals("9999999999999999999999999999999999999", comparison.getValue()); + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbRequestParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbRequestParserTest.java new file mode 100644 index 0000000000..5ab993c983 --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbRequestParserTest.java @@ -0,0 +1,361 @@ +package org.evomaster.client.java.controller.dynamodb; + +import org.evomaster.client.java.controller.dynamodb.operations.*; +import org.evomaster.client.java.controller.dynamodb.operations.comparison.*; +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import software.amazon.awssdk.services.dynamodb.model.*; + +import static org.junit.jupiter.api.Assertions.*; + + +public class DynamoDbRequestParserTest extends DynamoDbTestBase { + + private final DynamoDbRequestParser parser = new DynamoDbRequestParser(); + + @Test + public void testParseByTableGuards() { + assertTrue(parser.parseByTable(null, DynamoDbOperationNames.QUERY).isEmpty()); + assertTrue(parser.parseByTable(QueryRequest.builder().tableName("players").build(), null).isEmpty()); + + QueryRequest request = QueryRequest.builder() + .tableName("players") + .keyConditionExpression("id = :id") + .expressionAttributeValues(attributeValues(":id", stringValue("messi-10"))) + .build(); + + QueryOperation operation = parser.parseByTable(request, DynamoDbOperationNames.QUERY).get("players"); + assertComparison(operation, EqualsOperation.class, "id", "messi-10"); + } + + @Test + public void testQueryParsesKeyAndFilterExpressions() { + QueryRequest request = QueryRequest.builder() + .tableName("players") + .keyConditionExpression("#pk = :id") + .filterExpression("(age >= :min AND begins_with(#email, :prefix)) OR attribute_not_exists(#deleted)") + .expressionAttributeNames(names( + "#pk", "id", + "#email", "email", + "#deleted", "deletedAt" + )) + .expressionAttributeValues(attributeValues( + ":id", stringValue("messi-10"), + ":min", numberValue("38"), + ":prefix", stringValue("messi@") + )) + .build(); + + QueryOperation operation = parser.parseByTable(request, DynamoDbOperationNames.QUERY).get("players"); + AndOperation topAnd = castAs(operation, AndOperation.class); + assertEquals(2, topAnd.getConditions().size()); + + assertComparison(topAnd.getConditions().get(0), EqualsOperation.class, "id", "messi-10"); + + OrOperation filterOr = castAs(topAnd.getConditions().get(1), OrOperation.class); + assertEquals(2, filterOr.getConditions().size()); + + AndOperation nestedAnd = castAs(filterOr.getConditions().get(0), AndOperation.class); + assertEquals(2, nestedAnd.getConditions().size()); + assertComparison(nestedAnd.getConditions().get(0), GreaterThanEqualsOperation.class, "age", 38L); + + BeginsWithOperation beginsWith = castAs(nestedAnd.getConditions().get(1), BeginsWithOperation.class); + assertEquals("email", beginsWith.getFieldName()); + assertEquals("messi@", beginsWith.getPrefix()); + + ExistsOperation notExists = castAs(filterOr.getConditions().get(1), ExistsOperation.class); + assertEquals("deletedAt", notExists.getFieldName()); + assertFalse(notExists.isExists()); + } + + @Test + public void testQueryParsesLiteralValues() { + QueryRequest request = QueryRequest.builder() + .tableName("players") + .keyConditionExpression("id = 'ronaldo-7'") + .filterExpression("caps > 1.5e2 AND active = TRUE AND note = NULL") + .build(); + + QueryOperation operation = parser.parseByTable(request, DynamoDbOperationNames.QUERY).get("players"); + AndOperation topAnd = castAs(operation, AndOperation.class); + assertEquals(2, topAnd.getConditions().size()); + assertComparison(topAnd.getConditions().get(0), EqualsOperation.class, "id", "ronaldo-7"); + + AndOperation filterAnd = castAs(topAnd.getConditions().get(1), AndOperation.class); + assertEquals(3, filterAnd.getConditions().size()); + + ComparisonOperation greater = castAs(filterAnd.getConditions().get(0), GreaterThanOperation.class); + assertEquals("caps", greater.getFieldName()); + assertInstanceOf(Double.class, greater.getValue()); + assertEquals(150.0, (Double) greater.getValue(), 0.000001); + + assertComparison(filterAnd.getConditions().get(1), EqualsOperation.class, "active", true); + assertComparison(filterAnd.getConditions().get(2), EqualsOperation.class, "note", null); + } + + @Test + public void testScanParsesFilterOnly() { + ScanRequest request = ScanRequest.builder() + .tableName("players") + .filterExpression("contains(tags, :tag) AND size(tags) >= :n") + .expressionAttributeValues(attributeValues( + ":tag", stringValue("world-cup"), + ":n", numberValue("2") + )) + .build(); + + QueryOperation operation = parser.parseByTable(request, DynamoDbOperationNames.SCAN).get("players"); + AndOperation and = castAs(operation, AndOperation.class); + assertEquals(2, and.getConditions().size()); + + ContainsOperation contains = castAs(and.getConditions().get(0), ContainsOperation.class); + assertEquals("tags", contains.getFieldName()); + assertEquals("world-cup", contains.getExpectedValue()); + + SizeOperation size = castAs(and.getConditions().get(1), SizeOperation.class); + assertEquals("tags", size.getFieldName()); + assertEquals(2L, size.getExpectedValue()); + } + + @Test + public void testScanWithoutFilterReturnsEmptyMap() { + ScanRequest request = ScanRequest.builder() + .tableName("players") + .build(); + + assertTrue(parser.parseByTable(request, DynamoDbOperationNames.SCAN).isEmpty()); + } + + @Test + public void testScanWithBlankTableReturnsEmptyMap() { + ScanRequest request = ScanRequest.builder() + .tableName(" ") + .filterExpression("id = :id") + .expressionAttributeValues(attributeValues(":id", stringValue("messi-10"))) + .build(); + + assertTrue(parser.parseByTable(request, DynamoDbOperationNames.SCAN).isEmpty()); + } + + @Test + public void testPutItemParsesConditionOnly() { + PutItemRequest request = PutItemRequest.builder() + .tableName("players") + .conditionExpression("attribute_exists(#status) AND #status <> :old") + .expressionAttributeNames(names("#status", "status")) + .expressionAttributeValues(attributeValues(":old", stringValue("RETIRED"))) + .build(); + + QueryOperation operation = parser.parseByTable(request, DynamoDbOperationNames.PUT_ITEM).get("players"); + AndOperation and = castAs(operation, AndOperation.class); + assertEquals(2, and.getConditions().size()); + + ExistsOperation exists = castAs(and.getConditions().get(0), ExistsOperation.class); + assertEquals("status", exists.getFieldName()); + assertTrue(exists.isExists()); + + assertComparison(and.getConditions().get(1), NotEqualsOperation.class, "status", "RETIRED"); + } + + @Test + public void testPutItemWithBlankTableReturnsEmptyMap() { + PutItemRequest request = PutItemRequest.builder() + .tableName(" ") + .conditionExpression("id = :id") + .expressionAttributeValues(attributeValues(":id", stringValue("messi-10"))) + .build(); + + assertTrue(parser.parseByTable(request, DynamoDbOperationNames.PUT_ITEM).isEmpty()); + } + + @Test + public void testDeleteItemCombinesKeyAndCondition() { + DeleteItemRequest request = DeleteItemRequest.builder() + .tableName("players") + .key(attributeValues( + "id", stringValue("maradona-10"), + "tenant", stringValue("Argentina") + )) + .conditionExpression("version = :v") + .expressionAttributeValues(attributeValues(":v", numberValue("7"))) + .build(); + + QueryOperation operation = parser.parseByTable(request, DynamoDbOperationNames.DELETE_ITEM).get("players"); + AndOperation topAnd = castAs(operation, AndOperation.class); + assertEquals(2, topAnd.getConditions().size()); + + AndOperation keyAnd = castAs(topAnd.getConditions().get(0), AndOperation.class); + assertEquals(2, keyAnd.getConditions().size()); + assertComparison(keyAnd.getConditions().get(0), EqualsOperation.class, "id", "maradona-10"); + assertComparison(keyAnd.getConditions().get(1), EqualsOperation.class, "tenant", "Argentina"); + + assertComparison(topAnd.getConditions().get(1), EqualsOperation.class, "version", 7L); + } + + @Test + public void testUpdateItemCombinesKeyAndCondition() { + UpdateItemRequest request = UpdateItemRequest.builder() + .tableName("players") + .key(attributeValues("id", stringValue("ronaldo-7"))) + .conditionExpression("#age BETWEEN :l AND :u") + .expressionAttributeNames(names("#age", "age")) + .expressionAttributeValues(attributeValues( + ":l", numberValue("38"), + ":u", numberValue("41") + )) + .build(); + + QueryOperation operation = parser.parseByTable(request, DynamoDbOperationNames.UPDATE_ITEM).get("players"); + AndOperation and = castAs(operation, AndOperation.class); + assertEquals(2, and.getConditions().size()); + + assertComparison(and.getConditions().get(0), EqualsOperation.class, "id", "ronaldo-7"); + BetweenOperation between = castAs(and.getConditions().get(1), BetweenOperation.class); + assertEquals("age", between.getFieldName()); + assertEquals(38L, between.getLowerBound()); + assertEquals(41L, between.getUpperBound()); + } + + @Test + public void testGetItemParsesCompositeKey() { + GetItemRequest request = GetItemRequest.builder() + .tableName("players") + .key(attributeValues( + "id", stringValue("pele-10"), + "tenant", stringValue("brazil") + )) + .build(); + + QueryOperation operation = parser.parseByTable(request, DynamoDbOperationNames.GET_ITEM).get("players"); + AndOperation and = castAs(operation, AndOperation.class); + assertEquals(2, and.getConditions().size()); + assertComparison(and.getConditions().get(0), EqualsOperation.class, "id", "pele-10"); + assertComparison(and.getConditions().get(1), EqualsOperation.class, "tenant", "brazil"); + } + + @Test + public void testGetItemWithoutKeyReturnsEmptyMap() { + GetItemRequest request = GetItemRequest.builder() + .tableName("players") + .build(); + + assertTrue(parser.parseByTable(request, DynamoDbOperationNames.GET_ITEM).isEmpty()); + } + + @Test + public void testBatchGetParsesEachTableAndSkipsInvalidTableNames() { + Map requestItems = new LinkedHashMap<>(); + requestItems.put("players", KeysAndAttributes.builder().keys(Arrays.asList( + attributeValues("id", stringValue("messi-10")), + attributeValues("id", stringValue("ronaldo-7")) + )).build()); + requestItems.put("matches", KeysAndAttributes.builder().keys(Collections.singletonList( + attributeValues( + "matchId", stringValue("wc-final-1986"), + "tenant", stringValue("Mexico") + ) + )).build()); + requestItems.put("", KeysAndAttributes.builder().keys(Collections.singletonList( + attributeValues("id", stringValue("pele-10")) + )).build()); + + BatchGetItemRequest request = BatchGetItemRequest.builder() + .requestItems(requestItems) + .build(); + + Map parsed = parser.parseByTable(request, DynamoDbOperationNames.BATCH_GET_ITEM); + assertEquals(2, parsed.size()); + + OrOperation players = castAs(parsed.get("players"), OrOperation.class); + assertEquals(2, players.getConditions().size()); + assertComparison(players.getConditions().get(0), EqualsOperation.class, "id", "messi-10"); + assertComparison(players.getConditions().get(1), EqualsOperation.class, "id", "ronaldo-7"); + + AndOperation matches = castAs(parsed.get("matches"), AndOperation.class); + assertEquals(2, matches.getConditions().size()); + assertComparison(matches.getConditions().get(0), EqualsOperation.class, "matchId", "wc-final-1986"); + assertComparison(matches.getConditions().get(1), EqualsOperation.class, "tenant", "Mexico"); + } + + @Test + public void testBatchGetWithEmptyKeysDoesNotAddTable() { + BatchGetItemRequest request = BatchGetItemRequest.builder() + .requestItems(Collections.singletonMap( + "players", + KeysAndAttributes.builder().keys(Collections.singletonList( + Collections.emptyMap() + )).build() + )) + .build(); + + assertTrue(parser.parseByTable(request, DynamoDbOperationNames.BATCH_GET_ITEM).isEmpty()); + } + + @Test + public void testBatchGetSkipsInvalidRequestItemsShapes() { + assertTrue(parser.parseByTable(new RequestWithNonMapRequestItems(), DynamoDbOperationNames.BATCH_GET_ITEM).isEmpty()); + assertTrue(parser.parseByTable(new RequestWithNonCollectionKeys(), DynamoDbOperationNames.BATCH_GET_ITEM).isEmpty()); + } + + @Test + public void testConditionWithTypeAndInParsing() { + PutItemRequest request = PutItemRequest.builder() + .tableName("players") + .conditionExpression("attribute_type(kind, S) AND status IN (:s1, 'GOAT', champion)") + .expressionAttributeValues(attributeValues(":s1", stringValue("LEGEND"))) + .build(); + + QueryOperation operation = parser.parseByTable(request, DynamoDbOperationNames.PUT_ITEM).get("players"); + AndOperation and = castAs(operation, AndOperation.class); + assertEquals(2, and.getConditions().size()); + + TypeOperation type = castAs(and.getConditions().get(0), TypeOperation.class); + assertEquals("kind", type.getFieldName()); + assertEquals("S", type.getExpectedType()); + + InOperation in = castAs(and.getConditions().get(1), InOperation.class); + assertEquals("status", in.getFieldName()); + assertEquals(Arrays.asList("LEGEND", "GOAT", "champion"), in.getValues()); + } + + @Test + public void testBlankTableNameReturnsEmptyMap() { + QueryRequest request = QueryRequest.builder() + .tableName(" ") + .keyConditionExpression("id = :id") + .expressionAttributeValues(attributeValues(":id", stringValue("messi-10"))) + .build(); + + assertTrue(parser.parseByTable(request, DynamoDbOperationNames.QUERY).isEmpty()); + } + + private static class RequestWithNonMapRequestItems { + @SuppressWarnings("unused") // will be invoked by reflection + public Object requestItems() { + return "not-a-map"; + } + } + + //Reflection will invoke fake class, method + private static class RequestWithNonCollectionKeys { + @SuppressWarnings("unused") // will be invoked by reflection + public Map requestItems() { + Map map = new LinkedHashMap<>(); + map.put("players", new NonCollectionKeysHolder()); + return map; + } + } + + //Reflection will invoke fake class, method + private static class NonCollectionKeysHolder { + @SuppressWarnings("unused") // will be invoked by reflection + public Object keys() { + return "not-a-collection"; + } + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbTestBase.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbTestBase.java new file mode 100644 index 0000000000..41c5e7ef9b --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbTestBase.java @@ -0,0 +1,65 @@ +package org.evomaster.client.java.controller.dynamodb; + +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; +import org.evomaster.client.java.controller.dynamodb.operations.comparison.ComparisonOperation; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.*; + +public abstract class DynamoDbTestBase { + + protected final Map values(Object... kv) { + return toMap(kv, value -> value); + } + + protected final Map names(Object... kv) { + return toMap(kv, String::valueOf); + } + + protected final Map attributeValues(Object... kv) { + return toMap(kv, value -> (AttributeValue) value); + } + + protected final AttributeValue stringValue(String value) { + return AttributeValue.builder().s(value).build(); + } + + protected final AttributeValue numberValue(String value) { + return AttributeValue.builder().n(value).build(); + } + + @SuppressWarnings({"rawtypes"}) + protected final void assertComparison( + QueryOperation operation, + Class expectedType, + String expectedField, + Object expectedValue) { + assertNotNull(operation); + assertTrue(expectedType.isInstance(operation)); + ComparisonOperation comparison = (ComparisonOperation) operation; + assertEquals(expectedField, comparison.getFieldName()); + assertEquals(expectedValue, comparison.getValue()); + } + + protected final T castAs(QueryOperation operation, Class type) { + assertNotNull(operation); + assertTrue(type.isInstance(operation)); + return type.cast(operation); + } + + private static Map toMap(Object[] kv, Function valueMapper) { + if (kv.length % 2 != 0) { + throw new IllegalArgumentException("Expected an even number of key/value arguments"); + } + + Map map = new LinkedHashMap<>(); + for (int i = 0; i < kv.length; i += 2) { + map.put(String.valueOf(kv[i]), valueMapper.apply(kv[i + 1])); + } + return map; + } +} diff --git a/client-java/controller/src/test/resources/simplelogger.properties b/client-java/controller/src/test/resources/simplelogger.properties new file mode 100644 index 0000000000..cd90c2acb8 --- /dev/null +++ b/client-java/controller/src/test/resources/simplelogger.properties @@ -0,0 +1 @@ +org.slf4j.simpleLogger.defaultLogLevel=warn diff --git a/client-java/instrumentation/pom.xml b/client-java/instrumentation/pom.xml index 141b78798b..2a530fef8a 100644 --- a/client-java/instrumentation/pom.xml +++ b/client-java/instrumentation/pom.xml @@ -143,9 +143,8 @@ test - org.hibernate + org.hibernate.validator hibernate-validator - 6.2.0.Final test diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/DynamoDbCommand.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/DynamoDbCommand.java index 12df40ca87..c7cf2cb9cd 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/DynamoDbCommand.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/DynamoDbCommand.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * Info related to DynamoDB command execution. @@ -18,7 +19,7 @@ public class DynamoDbCommand implements Serializable { /** * Name of the operation that was executed */ - private final String operationName; + private final DynamoDbOperationNames operationName; /** * Actual executed operation */ @@ -32,11 +33,11 @@ public class DynamoDbCommand implements Serializable { */ private final long executionTime; - public DynamoDbCommand(List tableNames, String operationName, Object request, boolean successfullyExecuted, long executionTime) { + public DynamoDbCommand(List tableNames, DynamoDbOperationNames operationName, Object request, boolean successfullyExecuted, long executionTime) { this.tableNames = tableNames == null ? Collections.emptyList() : Collections.unmodifiableList(new ArrayList<>(tableNames)); - this.operationName = operationName; + this.operationName = Objects.requireNonNull(operationName, "operationName cannot be null"); this.request = request; this.successfullyExecuted = successfullyExecuted; this.executionTime = executionTime; @@ -46,7 +47,7 @@ public List getTableNames() { return tableNames; } - public String getOperationName() { + public DynamoDbOperationNames getOperationName() { return operationName; } diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/DynamoDbOperationNames.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/DynamoDbOperationNames.java new file mode 100644 index 0000000000..00212eb22c --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/DynamoDbOperationNames.java @@ -0,0 +1,30 @@ +package org.evomaster.client.java.instrumentation; + +/** + * Shared DynamoDB API operation names used across instrumentation and parsing logic. + */ +public enum DynamoDbOperationNames { + + GET_ITEM("GetItem"), + BATCH_GET_ITEM("BatchGetItem"), + PUT_ITEM("PutItem"), + UPDATE_ITEM("UpdateItem"), + DELETE_ITEM("DeleteItem"), + QUERY("Query"), + SCAN("Scan"); + + private final String value; + + DynamoDbOperationNames(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/DynamoDbClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/DynamoDbClassReplacement.java index c78c215786..16a723df19 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/DynamoDbClassReplacement.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/DynamoDbClassReplacement.java @@ -1,6 +1,7 @@ package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; import org.evomaster.client.java.instrumentation.DynamoDbCommand; +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyCast; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass; @@ -26,14 +27,6 @@ */ public class DynamoDbClassReplacement { - //DynamoDB API method names do not change them. - public static final String METHOD_GET_ITEM = "GetItem"; - public static final String METHOD_BATCH_GET_ITEM = "BatchGetItem"; - public static final String METHOD_PUT_ITEM = "PutItem"; - public static final String METHOD_UPDATE_ITEM = "UpdateItem"; - public static final String METHOD_DELETE_ITEM = "DeleteItem"; - public static final String METHOD_QUERY = "Query"; - public static final String METHOD_SCAN = "Scan"; public static final String METHOD_TABLE_NAME = "tableName"; public static final String METHOD_REQUEST_ITEMS = "requestItems"; @@ -54,37 +47,37 @@ protected String getNameOfThirdPartyTargetClass() { @Replacement(type = ReplacementType.TRACKER, id = DDB_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.GetItemResponse") public static Object getItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.GetItemRequest") Object request) { - return handle(client, DDB_GET_ITEM, request, METHOD_GET_ITEM); + return handle(client, DDB_GET_ITEM, request, DynamoDbOperationNames.GET_ITEM); } @Replacement(type = ReplacementType.TRACKER, id = DDB_BATCH_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse") public static Object batchGetItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest") Object request) { - return handle(client, DDB_BATCH_GET_ITEM, request, METHOD_BATCH_GET_ITEM); + return handle(client, DDB_BATCH_GET_ITEM, request, DynamoDbOperationNames.BATCH_GET_ITEM); } @Replacement(type = ReplacementType.TRACKER, id = DDB_PUT_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.PutItemResponse") public static Object putItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.PutItemRequest") Object request) { - return handle(client, DDB_PUT_ITEM, request, METHOD_PUT_ITEM); + return handle(client, DDB_PUT_ITEM, request, DynamoDbOperationNames.PUT_ITEM); } @Replacement(type = ReplacementType.TRACKER, id = DDB_UPDATE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse") public static Object updateItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest") Object request) { - return handle(client, DDB_UPDATE_ITEM, request, METHOD_UPDATE_ITEM); + return handle(client, DDB_UPDATE_ITEM, request, DynamoDbOperationNames.UPDATE_ITEM); } @Replacement(type = ReplacementType.TRACKER, id = DDB_DELETE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.DeleteItemResponse") public static Object deleteItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest") Object request) { - return handle(client, DDB_DELETE_ITEM, request, METHOD_DELETE_ITEM); + return handle(client, DDB_DELETE_ITEM, request, DynamoDbOperationNames.DELETE_ITEM); } @Replacement(type = ReplacementType.TRACKER, id = DDB_QUERY, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.QueryResponse") public static Object query(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.QueryRequest") Object request) { - return handle(client, DDB_QUERY, request, METHOD_QUERY); + return handle(client, DDB_QUERY, request, DynamoDbOperationNames.QUERY); } @Replacement(type = ReplacementType.TRACKER, id = DDB_SCAN, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.ScanResponse") public static Object scan(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.ScanRequest") Object request) { - return handle(client, DDB_SCAN, request, METHOD_SCAN); + return handle(client, DDB_SCAN, request, DynamoDbOperationNames.SCAN); } } @@ -105,44 +98,44 @@ protected String getNameOfThirdPartyTargetClass() { @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") public static Object getItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.GetItemRequest") Object request) { - return handleAsync(client, DDB_ASYNC_GET_ITEM, request, METHOD_GET_ITEM); + return handleAsync(client, DDB_ASYNC_GET_ITEM, request, DynamoDbOperationNames.GET_ITEM); } @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_BATCH_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") public static Object batchGetItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest") Object request) { - return handleAsync( client, DDB_ASYNC_BATCH_GET_ITEM, request, METHOD_BATCH_GET_ITEM); + return handleAsync(client, DDB_ASYNC_BATCH_GET_ITEM, request, DynamoDbOperationNames.BATCH_GET_ITEM); } @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_PUT_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") public static Object putItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.PutItemRequest") Object request) { - return handleAsync(client, DDB_ASYNC_PUT_ITEM, request, METHOD_PUT_ITEM); + return handleAsync(client, DDB_ASYNC_PUT_ITEM, request, DynamoDbOperationNames.PUT_ITEM); } @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_UPDATE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") public static Object updateItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest") Object request) { - return handleAsync(client, DDB_ASYNC_UPDATE_ITEM, request, METHOD_UPDATE_ITEM); + return handleAsync(client, DDB_ASYNC_UPDATE_ITEM, request, DynamoDbOperationNames.UPDATE_ITEM); } @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_DELETE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") public static Object deleteItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest") Object request) { - return handleAsync(client, DDB_ASYNC_DELETE_ITEM, request, METHOD_DELETE_ITEM); + return handleAsync(client, DDB_ASYNC_DELETE_ITEM, request, DynamoDbOperationNames.DELETE_ITEM); } @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_QUERY, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") public static Object query(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.QueryRequest") Object request) { - return handleAsync(client, DDB_ASYNC_QUERY, request, METHOD_QUERY); + return handleAsync(client, DDB_ASYNC_QUERY, request, DynamoDbOperationNames.QUERY); } @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_SCAN, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") public static Object scan(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.ScanRequest") Object request) { - return handleAsync(client, DDB_ASYNC_SCAN, request, METHOD_SCAN); + return handleAsync(client, DDB_ASYNC_SCAN, request, DynamoDbOperationNames.SCAN); } } /** * Invoke the original synchronous client method and trace the command execution. */ - protected static Object handle(Object client, String id, Object request, String operationName) { + protected static Object handle(Object client, String id, Object request, DynamoDbOperationNames operationName) { long start = System.currentTimeMillis(); try { Method method = getOriginal(Sync.singleton, id, client); @@ -165,7 +158,7 @@ protected static Object handle(Object client, String id, Object request, String /** * Invoke the original asynchronous client method and trace completion status. */ - protected static Object handleAsync(Object client, String id, Object request, String operationName) { + protected static Object handleAsync(Object client, String id, Object request, DynamoDbOperationNames operationName) { long start = System.currentTimeMillis(); try { Method method = getOriginal(Async.singleton, id, client); diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/DynamoDbClassReplacementTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/DynamoDbClassReplacementTest.java index fd3e26bf99..cf0a79beb9 100644 --- a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/DynamoDbClassReplacementTest.java +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/DynamoDbClassReplacementTest.java @@ -2,6 +2,7 @@ import org.evomaster.client.java.instrumentation.AdditionalInfo; import org.evomaster.client.java.instrumentation.DynamoDbCommand; +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -118,7 +119,7 @@ public void testGetItem() { GetItemResponse result = (GetItemResponse) DynamoDbClassReplacement.Sync.getItem(syncClient, request); assertNotNull(result); - verifyInterception(Collections.singletonList(TABLE_NAME), "GetItem", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.GET_ITEM, request); } @Test @@ -131,7 +132,7 @@ public void testPutItem() { PutItemResponse result = (PutItemResponse) DynamoDbClassReplacement.Sync.putItem(syncClient, request); assertNotNull(result); - verifyInterception(Collections.singletonList(TABLE_NAME), "PutItem", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.PUT_ITEM, request); } @Test @@ -147,7 +148,7 @@ public void testUpdateItem() { UpdateItemResponse result = (UpdateItemResponse) DynamoDbClassReplacement.Sync.updateItem(syncClient, request); assertNotNull(result); - verifyInterception(Collections.singletonList(TABLE_NAME), "UpdateItem", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.UPDATE_ITEM, request); } @Test @@ -160,7 +161,7 @@ public void testDeleteItem() { DeleteItemResponse result = (DeleteItemResponse) DynamoDbClassReplacement.Sync.deleteItem(syncClient, request); assertNotNull(result); - verifyInterception(Collections.singletonList(TABLE_NAME), "DeleteItem", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.DELETE_ITEM, request); } @Test @@ -174,7 +175,7 @@ public void testQuery() { QueryResponse result = (QueryResponse) DynamoDbClassReplacement.Sync.query(syncClient, request); assertNotNull(result); - verifyInterception(Collections.singletonList(TABLE_NAME), "Query", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.QUERY, request); } @Test @@ -186,7 +187,7 @@ public void testScan() { ScanResponse result = (ScanResponse) DynamoDbClassReplacement.Sync.scan(syncClient, request); assertNotNull(result); - verifyInterception(Collections.singletonList(TABLE_NAME), "Scan", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.SCAN, request); } @Test @@ -205,7 +206,7 @@ public void testBatchGetItem() { BatchGetItemResponse result = (BatchGetItemResponse) DynamoDbClassReplacement.Sync.batchGetItem(syncClient, request); assertNotNull(result); - verifyInterception(Arrays.asList(TABLE_NAME, TABLE_NAME_SECOND), "BatchGetItem", request); + verifyInterception(Arrays.asList(TABLE_NAME, TABLE_NAME_SECOND), DynamoDbOperationNames.BATCH_GET_ITEM, request); } // --- Async Tests --- @@ -225,7 +226,7 @@ public void testGetItemAsync() { } catch (Exception e) { // ignore } - verifyInterception(Collections.singletonList(TABLE_NAME), "GetItem", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.GET_ITEM, request); } @Test @@ -243,7 +244,7 @@ public void testPutItemAsync() { } catch (Exception e) { // ignore } - verifyInterception(Collections.singletonList(TABLE_NAME), "PutItem", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.PUT_ITEM, request); } @Test @@ -264,7 +265,7 @@ public void testUpdateItemAsync() { } catch (Exception e) { // ignore } - verifyInterception(Collections.singletonList(TABLE_NAME), "UpdateItem", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.UPDATE_ITEM, request); } @Test @@ -282,7 +283,7 @@ public void testDeleteItemAsync() { } catch (Exception e) { // ignore } - verifyInterception(Collections.singletonList(TABLE_NAME), "DeleteItem", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.DELETE_ITEM, request); } @Test @@ -301,7 +302,7 @@ public void testQueryAsync() { } catch (Exception e) { // ignore } - verifyInterception(Collections.singletonList(TABLE_NAME), "Query", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.QUERY, request); } @Test @@ -318,7 +319,7 @@ public void testScanAsync() { } catch (Exception e) { // ignore } - verifyInterception(Collections.singletonList(TABLE_NAME), "Scan", request); + verifyInterception(Collections.singletonList(TABLE_NAME), DynamoDbOperationNames.SCAN, request); } @Test @@ -341,10 +342,10 @@ public void testBatchGetItemAsync() { } catch (Exception e) { // ignore } - verifyInterception(Arrays.asList(TABLE_NAME, TABLE_NAME_SECOND), "BatchGetItem", request); + verifyInterception(Arrays.asList(TABLE_NAME, TABLE_NAME_SECOND), DynamoDbOperationNames.BATCH_GET_ITEM, request); } - private void verifyInterception(List expectedTableNames, String expectedOperationName, Object expectedRequest) { + private void verifyInterception(List expectedTableNames, DynamoDbOperationNames expectedOperationName, Object expectedRequest) { List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); assertEquals(1, additionalInfoList.size()); Set dynamoDbCommands = additionalInfoList.get(0).getDynamoDbInfoData(); diff --git a/core-parent/pom.xml b/core-parent/pom.xml index d7926a4247..b41347ec3d 100644 --- a/core-parent/pom.xml +++ b/core-parent/pom.xml @@ -36,7 +36,7 @@ - org.hibernate + org.hibernate.validator hibernate-validator