From dddac08308d049bb691fe493aa5423e53b3d6ab9 Mon Sep 17 00:00:00 2001 From: aschenzle Date: Thu, 23 Apr 2026 15:37:22 -0700 Subject: [PATCH 1/7] DynamoDB operation modeling and parsers. --- client-java/controller/pom.xml | 41 +- .../dynamodb/DynamoDbConditionExpression.g4 | 123 ++++++ .../DynamoDbAttributeValueHelper.java | 178 +++++++++ .../dynamodb/DynamoDbComparisonType.java | 56 +++ .../dynamodb/DynamoDbExpressionParser.java | 351 +++++++++++++++++ .../dynamodb/DynamoDbReflectionHelper.java | 29 ++ .../dynamodb/DynamoDbRequestParser.java | 62 +++ .../dynamodb/operations/AndOperation.java | 16 + .../operations/BeginsWithOperation.java | 20 + .../dynamodb/operations/BetweenOperation.java | 26 ++ .../operations/ContainsOperation.java | 20 + .../dynamodb/operations/ExistsOperation.java | 21 + .../dynamodb/operations/InOperation.java | 22 ++ .../dynamodb/operations/NotOperation.java | 14 + .../dynamodb/operations/OrOperation.java | 16 + .../dynamodb/operations/QueryOperation.java | 7 + .../dynamodb/operations/SizeOperation.java | 28 ++ .../dynamodb/operations/TypeOperation.java | 20 + .../comparison/ComparisonOperation.java | 22 ++ .../comparison/EqualsOperation.java | 8 + .../GreaterThanEqualsOperation.java | 8 + .../comparison/GreaterThanOperation.java | 8 + .../comparison/LessThanEqualsOperation.java | 8 + .../comparison/LessThanOperation.java | 8 + .../comparison/NotEqualsOperation.java | 8 + .../parsers/BatchGetItemApiMethodParser.java | 61 +++ .../parsers/DeleteItemApiMethodParser.java | 16 + .../parsers/DynamoDbApiMethodParser.java | 16 + .../parsers/DynamoDbBaseApiMethodParser.java | 116 ++++++ .../parsers/GetItemApiMethodParser.java | 21 + .../parsers/PutItemApiMethodParser.java | 16 + .../parsers/QueryApiMethodParser.java | 39 ++ .../dynamodb/parsers/ScanApiMethodParser.java | 31 ++ .../parsers/UpdateItemApiMethodParser.java | 16 + .../dynamodb/parsers/WriteMethodParser.java | 28 ++ .../DynamoDbAttributeValueHelperTest.java | 183 +++++++++ .../DynamoDbExpressionParserTest.java | 123 ++++++ .../dynamodb/DynamoDbRequestParserTest.java | 361 ++++++++++++++++++ .../controller/dynamodb/DynamoDbTestBase.java | 65 ++++ .../test/resources/simplelogger.properties | 1 + client-java/instrumentation/pom.xml | 3 +- .../java/instrumentation/DynamoDbCommand.java | 9 +- .../DynamoDbOperationNames.java | 30 ++ .../DynamoDbClassReplacement.java | 41 +- .../DynamoDbClassReplacementTest.java | 31 +- core-parent/pom.xml | 2 +- 46 files changed, 2279 insertions(+), 50 deletions(-) create mode 100644 client-java/controller/src/main/antlr4/org/evomaster/client/java/controller/dynamodb/DynamoDbConditionExpression.g4 create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbAttributeValueHelper.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbComparisonType.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbExpressionParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbReflectionHelper.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbRequestParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/AndOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/BeginsWithOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/BetweenOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/ContainsOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/ExistsOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/InOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/NotOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/OrOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/QueryOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/SizeOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/TypeOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/ComparisonOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/EqualsOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/GreaterThanEqualsOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/GreaterThanOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/LessThanEqualsOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/LessThanOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/NotEqualsOperation.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/BatchGetItemApiMethodParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DeleteItemApiMethodParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DynamoDbApiMethodParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DynamoDbBaseApiMethodParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/GetItemApiMethodParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/PutItemApiMethodParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/QueryApiMethodParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/ScanApiMethodParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/UpdateItemApiMethodParser.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/WriteMethodParser.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbAttributeValueHelperTest.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbExpressionParserTest.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbRequestParserTest.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbTestBase.java create mode 100644 client-java/controller/src/test/resources/simplelogger.properties create mode 100644 client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/DynamoDbOperationNames.java 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..d55b85444c --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbAttributeValueHelper.java @@ -0,0 +1,178 @@ +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"; + + private DynamoDbAttributeValueHelper() { + } + + 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; + } + + @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; + } + + 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; + } + + 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; + } + + private static List toPlainList(Collection source) { + List converted = new ArrayList<>(source.size()); + for (Object element : source) { + converted.add(toPlainValue(element)); + } + return converted; + } + + 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; + } + + private static Set toBinarySet(Collection source) { + LinkedHashSet binaries = new LinkedHashSet<>(); + for (Object binary : source) { + binaries.add(toPlainBinary(binary)); + } + return binaries; + } + + 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..9acc303af5 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbComparisonType.java @@ -0,0 +1,56 @@ +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; + + 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); + } + + 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..c1b67c4712 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbExpressionParser.java @@ -0,0 +1,351 @@ +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); + } + + /** + * Parses and resolves a field path from Parser context. + * + * @param pathContext the parsed path context + * @return resolved field path with attribute-name aliases expanded + */ + private String parsePath(DynamoDbConditionExpressionParser.PathContext pathContext) { + return parseFieldName(pathContext.getText()); + } + + /** + * Resolves expression-attribute-name aliases in a dotted field path. + * + * @param token raw field token from the expression + * @return resolved field path + */ + 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)); + } + + 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(); + } + } + + 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(parsePath(ctx.path()), true); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitAttributeNotExistsPredicate(DynamoDbConditionExpressionParser.AttributeNotExistsPredicateContext ctx) { + return new ExistsOperation(parsePath(ctx.path()), false); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitAttributeTypePredicate(DynamoDbConditionExpressionParser.AttributeTypePredicateContext ctx) { + Object expectedType = parseValue(ctx.value()); + return new TypeOperation(parsePath(ctx.path()), expectedType == null ? null : String.valueOf(expectedType)); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitBeginsWithPredicate(DynamoDbConditionExpressionParser.BeginsWithPredicateContext ctx) { + return new BeginsWithOperation(parsePath(ctx.path()), parseValue(ctx.value())); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitContainsPredicate(DynamoDbConditionExpressionParser.ContainsPredicateContext ctx) { + return new ContainsOperation(parsePath(ctx.path()), parseValue(ctx.value())); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitSizePredicate(DynamoDbConditionExpressionParser.SizePredicateContext ctx) { + String field = parsePath(ctx.path()); + 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 = parsePath(ctx.path()); + 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<>(parsePath(ctx.path()), values); + } + + /** {@inheritDoc} */ + @Override + public QueryOperation visitComparisonPredicate(DynamoDbConditionExpressionParser.ComparisonPredicateContext ctx) { + String field = parsePath(ctx.path()); + 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..1a2e8792b0 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbReflectionHelper.java @@ -0,0 +1,29 @@ +package org.evomaster.client.java.controller.dynamodb; + +import java.lang.reflect.Method; + +/** + * Shared reflection helpers used by DynamoDB parser utilities. + */ +public final class DynamoDbReflectionHelper { + + private DynamoDbReflectionHelper() { + } + + 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; + } + } + + 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..67004beb9d --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbRequestParser.java @@ -0,0 +1,62 @@ +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; + + 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 the 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; + } + + 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..f1794713da --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/AndOperation.java @@ -0,0 +1,16 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +import java.util.List; + +public class AndOperation extends QueryOperation { + + private final List conditions; + + public AndOperation(List conditions) { + this.conditions = 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..043d6fa7de --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/BeginsWithOperation.java @@ -0,0 +1,20 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +public class BeginsWithOperation extends QueryOperation { + + private final String fieldName; + private final Object prefix; + + public BeginsWithOperation(String fieldName, Object prefix) { + this.fieldName = fieldName; + this.prefix = prefix; + } + + public String getFieldName() { + return fieldName; + } + + 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..839db62eca --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/BetweenOperation.java @@ -0,0 +1,26 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +public class BetweenOperation extends QueryOperation { + + private final String fieldName; + private final Object lowerBound; + private final Object upperBound; + + public BetweenOperation(String fieldName, Object lowerBound, Object upperBound) { + this.fieldName = fieldName; + this.lowerBound = lowerBound; + this.upperBound = upperBound; + } + + public String getFieldName() { + return fieldName; + } + + public Object getLowerBound() { + return lowerBound; + } + + 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..7bc35b5633 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/ContainsOperation.java @@ -0,0 +1,20 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +public class ContainsOperation extends QueryOperation { + + private final String fieldName; + private final Object expectedValue; + + public ContainsOperation(String fieldName, Object expectedValue) { + this.fieldName = fieldName; + this.expectedValue = expectedValue; + } + + public String getFieldName() { + return fieldName; + } + + 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..33bf30a4f8 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/ExistsOperation.java @@ -0,0 +1,21 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +public class ExistsOperation extends QueryOperation { + + private final String fieldName; + //true = exists, false = not exists + private final boolean exists; + + public ExistsOperation(String fieldName, boolean exists) { + this.fieldName = fieldName; + this.exists = exists; + } + + public String getFieldName() { + return fieldName; + } + + 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..3b3f444f2b --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/InOperation.java @@ -0,0 +1,22 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +import java.util.List; + +public class InOperation extends QueryOperation { + + private final String fieldName; + private final List values; + + public InOperation(String fieldName, List values) { + this.fieldName = fieldName; + this.values = values; + } + + public String getFieldName() { + return fieldName; + } + + 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..075a6060a1 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/NotOperation.java @@ -0,0 +1,14 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +public class NotOperation extends QueryOperation { + + private final QueryOperation condition; + + public NotOperation(QueryOperation condition) { + this.condition = 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..de66cdc773 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/OrOperation.java @@ -0,0 +1,16 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +import java.util.List; + +public class OrOperation extends QueryOperation { + + private final List conditions; + + public OrOperation(List conditions) { + this.conditions = 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..a184d19695 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/SizeOperation.java @@ -0,0 +1,28 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +import org.evomaster.client.java.controller.dynamodb.DynamoDbComparisonType; + +public class SizeOperation extends QueryOperation { + + private final String fieldName; + private final DynamoDbComparisonType comparator; + private final Object expectedValue; + + public SizeOperation(String fieldName, DynamoDbComparisonType comparator, Object expectedValue) { + this.fieldName = fieldName; + this.comparator = comparator; + this.expectedValue = expectedValue; + } + + public String getFieldName() { + return fieldName; + } + + public DynamoDbComparisonType getComparator() { + return comparator; + } + + 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..fa1210de0f --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/TypeOperation.java @@ -0,0 +1,20 @@ +package org.evomaster.client.java.controller.dynamodb.operations; + +public class TypeOperation extends QueryOperation { + + private final String fieldName; + private final String expectedType; + + public TypeOperation(String fieldName, String expectedType) { + this.fieldName = fieldName; + this.expectedType = expectedType; + } + + public String getFieldName() { + return fieldName; + } + + 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..da752a7442 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/ComparisonOperation.java @@ -0,0 +1,22 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; + +public abstract class ComparisonOperation extends QueryOperation { + + private final String fieldName; + private final V value; + + ComparisonOperation(String fieldName, V value) { + this.fieldName = fieldName; + this.value = value; + } + + public String getFieldName() { + return fieldName; + } + + 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..785cb179d8 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/EqualsOperation.java @@ -0,0 +1,8 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +public class EqualsOperation extends ComparisonOperation { + + 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..c0fb797809 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/GreaterThanEqualsOperation.java @@ -0,0 +1,8 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +public class GreaterThanEqualsOperation extends ComparisonOperation { + + 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..dc3ca3d79e --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/GreaterThanOperation.java @@ -0,0 +1,8 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +public class GreaterThanOperation extends ComparisonOperation { + + 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..ee1bacc5e9 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/LessThanEqualsOperation.java @@ -0,0 +1,8 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +public class LessThanEqualsOperation extends ComparisonOperation { + + 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..8e2db85375 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/LessThanOperation.java @@ -0,0 +1,8 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +public class LessThanOperation extends ComparisonOperation { + + 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..b2a5e85312 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/comparison/NotEqualsOperation.java @@ -0,0 +1,8 @@ +package org.evomaster.client.java.controller.dynamodb.operations.comparison; + +public class NotEqualsOperation extends ComparisonOperation { + + 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..a978665848 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/BatchGetItemApiMethodParser.java @@ -0,0 +1,61 @@ +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; + +public class BatchGetItemApiMethodParser extends DynamoDbBaseApiMethodParser { + + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.BATCH_GET_ITEM; + } + + @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; + } + + 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..ef3d2006a5 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DeleteItemApiMethodParser.java @@ -0,0 +1,16 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +public class DeleteItemApiMethodParser extends WriteMethodParser { + + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.DELETE_ITEM; + } + + @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..ea2ef5e821 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DynamoDbApiMethodParser.java @@ -0,0 +1,16 @@ +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 { + + DynamoDbOperationNames apiMethodName(); + + 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..91f206dc06 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/DynamoDbBaseApiMethodParser.java @@ -0,0 +1,116 @@ +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; + +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"; + + protected QueryOperation parseExpression( + String expression, + Map expressionAttributeNames, + Map expressionAttributeValues) { + return new DynamoDbExpressionParser().parse(expression, expressionAttributeNames, expressionAttributeValues); + } + + protected QueryOperation parseKeyCondition(Object request) { + Object keyObj = invokeNoArg(request, METHOD_KEY); + return buildEqualsFromMap(DynamoDbAttributeValueHelper.toPlainMap(keyObj)); + } + + 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); + } + + protected QueryOperation combineWithAnd(QueryOperation left, QueryOperation right) { + return combineWithAnd(Arrays.asList(left, right)); + } + + protected QueryOperation combineWithAnd(List conditions) { + return combine(conditions, AndOperation::new); + } + + 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); + } + + 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; + } + + protected Map readValueMap(Object request) { + Object raw = invokeNoArg(request, METHOD_EXPRESSION_ATTRIBUTE_VALUES); + return DynamoDbAttributeValueHelper.toPlainMap(raw); + } + + protected String readString(Object target, String methodName) { + Object value = invokeNoArg(target, methodName); + return value == null ? null : String.valueOf(value); + } + + protected String readValidTableName(Object request) { + String tableName = readString(request, METHOD_TABLE_NAME); + if (tableName == null || tableName.trim().isEmpty()) { + return null; + } + return tableName; + } + + 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..66f7dcce5f --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/GetItemApiMethodParser.java @@ -0,0 +1,21 @@ +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; + +public class GetItemApiMethodParser extends DynamoDbBaseApiMethodParser { + + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.GET_ITEM; + } + + @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..6f5889520b --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/PutItemApiMethodParser.java @@ -0,0 +1,16 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +public class PutItemApiMethodParser extends WriteMethodParser { + + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.PUT_ITEM; + } + + @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..83d113927f --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/QueryApiMethodParser.java @@ -0,0 +1,39 @@ +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; + +public class QueryApiMethodParser extends DynamoDbBaseApiMethodParser { + + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.QUERY; + } + + @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..af760f1159 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/ScanApiMethodParser.java @@ -0,0 +1,31 @@ +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; + +public class ScanApiMethodParser extends DynamoDbBaseApiMethodParser { + + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.SCAN; + } + + @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..e212909048 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/UpdateItemApiMethodParser.java @@ -0,0 +1,16 @@ +package org.evomaster.client.java.controller.dynamodb.parsers; + +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; + +public class UpdateItemApiMethodParser extends WriteMethodParser { + + @Override + public DynamoDbOperationNames apiMethodName() { + return DynamoDbOperationNames.UPDATE_ITEM; + } + + @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..f4be7e131a --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/parsers/WriteMethodParser.java @@ -0,0 +1,28 @@ +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; + +abstract class WriteMethodParser extends DynamoDbBaseApiMethodParser { + + protected abstract boolean requiresKeyCondition(); + + @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 From 4477f1a0f369795aab8ceb8b8a74ca180e103f12 Mon Sep 17 00:00:00 2001 From: aschenzle Date: Thu, 23 Apr 2026 16:19:30 -0700 Subject: [PATCH 2/7] Adding javadoc --- .../DynamoDbAttributeValueHelper.java | 53 +++++++++++++ .../dynamodb/DynamoDbComparisonType.java | 13 ++++ .../dynamodb/DynamoDbExpressionParser.java | 6 ++ .../dynamodb/DynamoDbReflectionHelper.java | 17 +++++ .../dynamodb/DynamoDbRequestParser.java | 11 ++- .../dynamodb/operations/AndOperation.java | 13 ++++ .../operations/BeginsWithOperation.java | 9 +++ .../dynamodb/operations/BetweenOperation.java | 10 +++ .../operations/ContainsOperation.java | 9 +++ .../dynamodb/operations/ExistsOperation.java | 10 +++ .../dynamodb/operations/InOperation.java | 11 +++ .../dynamodb/operations/NotOperation.java | 8 ++ .../dynamodb/operations/OrOperation.java | 13 ++++ .../dynamodb/operations/SizeOperation.java | 10 +++ .../dynamodb/operations/TypeOperation.java | 9 +++ .../comparison/ComparisonOperation.java | 11 +++ .../comparison/EqualsOperation.java | 11 +++ .../GreaterThanEqualsOperation.java | 11 +++ .../comparison/GreaterThanOperation.java | 11 +++ .../comparison/LessThanEqualsOperation.java | 11 +++ .../comparison/LessThanOperation.java | 11 +++ .../comparison/NotEqualsOperation.java | 11 +++ .../parsers/BatchGetItemApiMethodParser.java | 15 ++++ .../parsers/DeleteItemApiMethodParser.java | 9 +++ .../parsers/DynamoDbApiMethodParser.java | 11 +++ .../parsers/DynamoDbBaseApiMethodParser.java | 75 +++++++++++++++++++ .../parsers/GetItemApiMethodParser.java | 9 +++ .../parsers/PutItemApiMethodParser.java | 9 +++ .../parsers/QueryApiMethodParser.java | 9 +++ .../dynamodb/parsers/ScanApiMethodParser.java | 9 +++ .../parsers/UpdateItemApiMethodParser.java | 9 +++ .../dynamodb/parsers/WriteMethodParser.java | 11 +++ 32 files changed, 444 insertions(+), 1 deletion(-) 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 index d55b85444c..1ac2a78c64 100644 --- 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 @@ -33,9 +33,18 @@ public final class DynamoDbAttributeValueHelper { 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(); @@ -50,6 +59,12 @@ public static Map toPlainMap(Object source) { 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) { @@ -119,6 +134,12 @@ public static Object toPlainValue(Object value) { 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(); @@ -130,6 +151,14 @@ private static Object toPlainBinary(Object value) { 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); @@ -137,6 +166,12 @@ private static Object readIfPresent(Object target, String hasMethod, String valu 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) { @@ -145,6 +180,12 @@ private static List toPlainList(Collection source) { 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) { @@ -155,6 +196,12 @@ private static Set toNumberSet(Collection source) { 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) { @@ -163,6 +210,12 @@ private static Set toBinarySet(Collection source) { 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) 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 index 9acc303af5..f9d4860a4f 100644 --- 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 @@ -13,6 +13,12 @@ public enum DynamoDbComparisonType { 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; @@ -35,6 +41,13 @@ public static DynamoDbComparisonType fromToken(String token) { throw new IllegalArgumentException("Unsupported comparator token: " + token); } + /** + * Creates a comparison operation instance for this comparison type. + * + * @param fieldName compared field name/path + * @param value comparison value + * @return concrete comparison operation + */ public ComparisonOperation toOperation(String fieldName, Object value) { switch (this) { case EQUALS: 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 index c1b67c4712..1b943aa374 100644 --- 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 @@ -171,6 +171,9 @@ private QueryOperation mergeOr(QueryOperation left, QueryOperation right) { return new OrOperation(Arrays.asList(left, right)); } + /** + * Visitor that converts grammar value nodes into Java values. + */ private class ValueVisitor extends DynamoDbConditionExpressionBaseVisitor { /** {@inheritDoc} */ @@ -211,6 +214,9 @@ public Object visitIdentifierValue(DynamoDbConditionExpressionParser.IdentifierV } } + /** + * Visitor that converts grammar predicate nodes into query operations. + */ private class OperationVisitor extends DynamoDbConditionExpressionBaseVisitor { /** {@inheritDoc} */ 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 index 1a2e8792b0..c042178e00 100644 --- 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 @@ -7,9 +7,19 @@ */ 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; @@ -22,6 +32,13 @@ public static Object invokeNoArg(Object target, String methodName) { } } + /** + * 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 index 67004beb9d..60d0576d3b 100644 --- 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 @@ -15,6 +15,9 @@ 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()); @@ -28,7 +31,7 @@ public DynamoDbRequestParser() { } /** - * Entry-point parser used by the handler. + * 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. @@ -47,6 +50,12 @@ public Map parseByTable(Object request, DynamoDbOperatio 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(); 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 index f1794713da..00a29020ac 100644 --- 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 @@ -2,14 +2,27 @@ 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 index 043d6fa7de..d8ad45ebbc 100644 --- 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 @@ -1,10 +1,19 @@ 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 attribute path + * @param prefix expected prefix + */ public BeginsWithOperation(String fieldName, Object prefix) { this.fieldName = fieldName; this.prefix = 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 index 839db62eca..81bb200a11 100644 --- 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 @@ -1,11 +1,21 @@ 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 attribute path + * @param lowerBound lower bound value + * @param upperBound upper bound value + */ public BetweenOperation(String fieldName, Object lowerBound, Object upperBound) { this.fieldName = fieldName; this.lowerBound = lowerBound; 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 index 7bc35b5633..5b7574c6b5 100644 --- 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 @@ -1,10 +1,19 @@ 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 attribute path + * @param expectedValue expected contained value + */ public ContainsOperation(String fieldName, Object expectedValue) { this.fieldName = fieldName; this.expectedValue = 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 index 33bf30a4f8..50b428a192 100644 --- 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 @@ -1,11 +1,21 @@ 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 attribute path + * @param exists {@code true} for exists, {@code false} for not-exists + */ public ExistsOperation(String fieldName, boolean exists) { this.fieldName = fieldName; this.exists = 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 index 3b3f444f2b..473a685dd5 100644 --- 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 @@ -2,11 +2,22 @@ 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 attribute path + * @param values candidate values + */ public InOperation(String fieldName, List values) { this.fieldName = fieldName; this.values = 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 index 075a6060a1..e9e8403f83 100644 --- 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 @@ -1,9 +1,17 @@ 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; } 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 index de66cdc773..0f9eb05460 100644 --- 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 @@ -2,14 +2,27 @@ 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/SizeOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/SizeOperation.java index a184d19695..6d94b3d3fc 100644 --- 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 @@ -2,12 +2,22 @@ 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 attribute path + * @param comparator comparison operator + * @param expectedValue expected value + */ public SizeOperation(String fieldName, DynamoDbComparisonType comparator, Object expectedValue) { this.fieldName = fieldName; this.comparator = comparator; 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 index fa1210de0f..c66adc1b59 100644 --- 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 @@ -1,10 +1,19 @@ 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 attribute path + * @param expectedType expected DynamoDB type token + */ public TypeOperation(String fieldName, String expectedType) { this.fieldName = fieldName; this.expectedType = 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 index da752a7442..73d5f5d6f6 100644 --- 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 @@ -2,11 +2,22 @@ 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 or path + * @param value comparison value + */ ComparisonOperation(String fieldName, V value) { this.fieldName = fieldName; this.value = 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 index 785cb179d8..8bfe951c15 100644 --- 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 @@ -1,7 +1,18 @@ 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 or path + * @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 index c0fb797809..e4dcc8c501 100644 --- 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 @@ -1,7 +1,18 @@ 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 or path + * @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 index dc3ca3d79e..35afe268c8 100644 --- 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 @@ -1,7 +1,18 @@ 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 or path + * @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 index ee1bacc5e9..310ea7276b 100644 --- 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 @@ -1,7 +1,18 @@ 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 or path + * @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 index 8e2db85375..ab4c16f6df 100644 --- 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 @@ -1,7 +1,18 @@ 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 or path + * @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 index b2a5e85312..9da15b5b7f 100644 --- 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 @@ -1,7 +1,18 @@ 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 or path + * @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 index a978665848..70b12cbd3a 100644 --- 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 @@ -9,13 +9,22 @@ 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) { @@ -55,6 +64,12 @@ public Map parseRequest(Object request) { 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 index ef3d2006a5..63e8a553ce 100644 --- 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 @@ -2,13 +2,22 @@ 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 index ea2ef5e821..be18a58124 100644 --- 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 @@ -10,7 +10,18 @@ */ 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 index 91f206dc06..9637148444 100644 --- 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 @@ -11,6 +11,9 @@ 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. @@ -24,6 +27,14 @@ abstract class DynamoDbBaseApiMethodParser implements DynamoDbApiMethodParser { 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, @@ -31,11 +42,23 @@ protected QueryOperation parseExpression( 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; @@ -46,14 +69,34 @@ protected QueryOperation buildEqualsFromMap(Map values) { 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) { @@ -71,6 +114,12 @@ protected QueryOperation combine(List conditions, Function readNameMap(Object request) { Object raw = invokeNoArg(request, METHOD_EXPRESSION_ATTRIBUTE_NAMES); if (!(raw instanceof Map)) { @@ -86,16 +135,35 @@ protected Map readNameMap(Object request) { 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()) { @@ -104,6 +172,13 @@ protected String readValidTableName(Object request) { 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(); 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 index 66f7dcce5f..e6dac06531 100644 --- 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 @@ -5,13 +5,22 @@ 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); 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 index 6f5889520b..35619e3c72 100644 --- 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 @@ -2,13 +2,22 @@ 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 index 83d113927f..2003448fe9 100644 --- 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 @@ -6,13 +6,22 @@ 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); 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 index af760f1159..9da73906b0 100644 --- 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 @@ -6,13 +6,22 @@ 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); 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 index e212909048..e215541607 100644 --- 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 @@ -2,13 +2,22 @@ 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 index f4be7e131a..49bb00422d 100644 --- 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 @@ -5,10 +5,21 @@ 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); From 22f85da2f98eb44e57866684369ec46c2287a394 Mon Sep 17 00:00:00 2001 From: aschenzle Date: Fri, 24 Apr 2026 18:01:12 -0700 Subject: [PATCH 3/7] Adding Javadoc to all methods. Removing unnecessary function. --- .../dynamodb/DynamoDbComparisonType.java | 2 +- .../dynamodb/DynamoDbExpressionParser.java | 34 +++++++------------ .../operations/BeginsWithOperation.java | 8 ++++- .../dynamodb/operations/BetweenOperation.java | 11 +++++- .../operations/ContainsOperation.java | 8 ++++- .../dynamodb/operations/ExistsOperation.java | 8 ++++- .../dynamodb/operations/InOperation.java | 8 ++++- .../dynamodb/operations/NotOperation.java | 3 ++ .../dynamodb/operations/SizeOperation.java | 11 +++++- .../dynamodb/operations/TypeOperation.java | 8 ++++- .../comparison/ComparisonOperation.java | 8 ++++- .../comparison/EqualsOperation.java | 2 +- .../GreaterThanEqualsOperation.java | 2 +- .../comparison/GreaterThanOperation.java | 2 +- .../comparison/LessThanEqualsOperation.java | 2 +- .../comparison/LessThanOperation.java | 2 +- .../comparison/NotEqualsOperation.java | 2 +- 17 files changed, 84 insertions(+), 37 deletions(-) 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 index f9d4860a4f..0a719d52d9 100644 --- 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 @@ -44,7 +44,7 @@ public static DynamoDbComparisonType fromToken(String token) { /** * Creates a comparison operation instance for this comparison type. * - * @param fieldName compared field name/path + * @param fieldName field name coming from DynamoDB expression/condition * @param value comparison value * @return concrete comparison operation */ 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 index 1b943aa374..468a0867da 100644 --- 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 @@ -77,20 +77,10 @@ private void prepareParser(Parser parser) { } /** - * Parses and resolves a field path from Parser context. + * Resolves expression-attribute-name aliases in a dotted field name. * - * @param pathContext the parsed path context - * @return resolved field path with attribute-name aliases expanded - */ - private String parsePath(DynamoDbConditionExpressionParser.PathContext pathContext) { - return parseFieldName(pathContext.getText()); - } - - /** - * Resolves expression-attribute-name aliases in a dotted field path. - * - * @param token raw field token from the expression - * @return resolved field path + * @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("\\."); @@ -274,38 +264,38 @@ public QueryOperation visitPredicatePrimary(DynamoDbConditionExpressionParser.Pr /** {@inheritDoc} */ @Override public QueryOperation visitAttributeExistsPredicate(DynamoDbConditionExpressionParser.AttributeExistsPredicateContext ctx) { - return new ExistsOperation(parsePath(ctx.path()), true); + return new ExistsOperation(parseFieldName(ctx.path().getText()), true); } /** {@inheritDoc} */ @Override public QueryOperation visitAttributeNotExistsPredicate(DynamoDbConditionExpressionParser.AttributeNotExistsPredicateContext ctx) { - return new ExistsOperation(parsePath(ctx.path()), false); + return new ExistsOperation(parseFieldName(ctx.path().getText()), false); } /** {@inheritDoc} */ @Override public QueryOperation visitAttributeTypePredicate(DynamoDbConditionExpressionParser.AttributeTypePredicateContext ctx) { Object expectedType = parseValue(ctx.value()); - return new TypeOperation(parsePath(ctx.path()), expectedType == null ? null : String.valueOf(expectedType)); + return new TypeOperation(parseFieldName(ctx.path().getText()), expectedType == null ? null : String.valueOf(expectedType)); } /** {@inheritDoc} */ @Override public QueryOperation visitBeginsWithPredicate(DynamoDbConditionExpressionParser.BeginsWithPredicateContext ctx) { - return new BeginsWithOperation(parsePath(ctx.path()), parseValue(ctx.value())); + return new BeginsWithOperation(parseFieldName(ctx.path().getText()), parseValue(ctx.value())); } /** {@inheritDoc} */ @Override public QueryOperation visitContainsPredicate(DynamoDbConditionExpressionParser.ContainsPredicateContext ctx) { - return new ContainsOperation(parsePath(ctx.path()), parseValue(ctx.value())); + return new ContainsOperation(parseFieldName(ctx.path().getText()), parseValue(ctx.value())); } /** {@inheritDoc} */ @Override public QueryOperation visitSizePredicate(DynamoDbConditionExpressionParser.SizePredicateContext ctx) { - String field = parsePath(ctx.path()); + String field = parseFieldName(ctx.path().getText()); DynamoDbComparisonType comparator = DynamoDbComparisonType.fromToken(ctx.comparator().getText()); Object expectedValue = parseValue(ctx.value()); return new SizeOperation(field, comparator, expectedValue); @@ -314,7 +304,7 @@ public QueryOperation visitSizePredicate(DynamoDbConditionExpressionParser.SizeP /** {@inheritDoc} */ @Override public QueryOperation visitBetweenPredicate(DynamoDbConditionExpressionParser.BetweenPredicateContext ctx) { - String field = parsePath(ctx.path()); + String field = parseFieldName(ctx.path().getText()); Object lower = parseValue(ctx.value(0)); Object upper = parseValue(ctx.value(1)); return new BetweenOperation(field, lower, upper); @@ -327,13 +317,13 @@ public QueryOperation visitInPredicate(DynamoDbConditionExpressionParser.InPredi for (DynamoDbConditionExpressionParser.ValueContext valueContext : ctx.value()) { values.add(parseValue(valueContext)); } - return new InOperation<>(parsePath(ctx.path()), values); + return new InOperation<>(parseFieldName(ctx.path().getText()), values); } /** {@inheritDoc} */ @Override public QueryOperation visitComparisonPredicate(DynamoDbConditionExpressionParser.ComparisonPredicateContext ctx) { - String field = parsePath(ctx.path()); + String field = parseFieldName(ctx.path().getText()); DynamoDbComparisonType comparator = DynamoDbComparisonType.fromToken(ctx.comparator().getText()); Object value = parseValue(ctx.value()); return comparator.toOperation(field, value); 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 index d8ad45ebbc..5e5bc8ce22 100644 --- 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 @@ -11,7 +11,7 @@ public class BeginsWithOperation extends QueryOperation { /** * Creates a begins-with operation. * - * @param fieldName attribute path + * @param fieldName field name coming from DynamoDB expression/condition * @param prefix expected prefix */ public BeginsWithOperation(String fieldName, Object prefix) { @@ -19,10 +19,16 @@ public BeginsWithOperation(String fieldName, Object prefix) { 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 index 81bb200a11..3e60a9c4ed 100644 --- 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 @@ -12,7 +12,7 @@ public class BetweenOperation extends QueryOperation { /** * Creates a BETWEEN operation. * - * @param fieldName attribute path + * @param fieldName field name coming from DynamoDB expression/condition * @param lowerBound lower bound value * @param upperBound upper bound value */ @@ -22,14 +22,23 @@ public BetweenOperation(String fieldName, Object lowerBound, Object upperBound) 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 index 5b7574c6b5..9580d026c2 100644 --- 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 @@ -11,7 +11,7 @@ public class ContainsOperation extends QueryOperation { /** * Creates a contains operation. * - * @param fieldName attribute path + * @param fieldName field name coming from DynamoDB expression/condition * @param expectedValue expected contained value */ public ContainsOperation(String fieldName, Object expectedValue) { @@ -19,10 +19,16 @@ public ContainsOperation(String fieldName, Object expectedValue) { 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 index 50b428a192..e45cc3e333 100644 --- 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 @@ -13,7 +13,7 @@ public class ExistsOperation extends QueryOperation { /** * Creates an existence operation. * - * @param fieldName attribute path + * @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) { @@ -21,10 +21,16 @@ public ExistsOperation(String fieldName, boolean exists) { 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 index 473a685dd5..df230f0fd9 100644 --- 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 @@ -15,7 +15,7 @@ public class InOperation extends QueryOperation { /** * Creates an IN operation. * - * @param fieldName attribute path + * @param fieldName field name coming from DynamoDB expression/condition * @param values candidate values */ public InOperation(String fieldName, List values) { @@ -23,10 +23,16 @@ public InOperation(String fieldName, List values) { 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 index e9e8403f83..3be8bc95dd 100644 --- 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 @@ -16,6 +16,9 @@ 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/SizeOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/operations/SizeOperation.java index 6d94b3d3fc..eac67f0c1c 100644 --- 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 @@ -14,7 +14,7 @@ public class SizeOperation extends QueryOperation { /** * Creates a size operation. * - * @param fieldName attribute path + * @param fieldName field name coming from DynamoDB expression/condition * @param comparator comparison operator * @param expectedValue expected value */ @@ -24,14 +24,23 @@ public SizeOperation(String fieldName, DynamoDbComparisonType comparator, Object 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 index c66adc1b59..8dbf35e230 100644 --- 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 @@ -11,7 +11,7 @@ public class TypeOperation extends QueryOperation { /** * Creates a type operation. * - * @param fieldName attribute path + * @param fieldName field name coming from DynamoDB expression/condition * @param expectedType expected DynamoDB type token */ public TypeOperation(String fieldName, String expectedType) { @@ -19,10 +19,16 @@ public TypeOperation(String fieldName, String expectedType) { 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 index 73d5f5d6f6..67aa85090e 100644 --- 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 @@ -15,7 +15,7 @@ public abstract class ComparisonOperation extends QueryOperation { /** * Creates a comparison operation. * - * @param fieldName field name or path + * @param fieldName field name coming from DynamoDB expression/condition * @param value comparison value */ ComparisonOperation(String fieldName, V value) { @@ -23,10 +23,16 @@ public abstract class ComparisonOperation extends QueryOperation { 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 index 8bfe951c15..8fe7b06036 100644 --- 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 @@ -10,7 +10,7 @@ public class EqualsOperation extends ComparisonOperation { /** * Creates an equality comparison operation. * - * @param fieldName field name or path + * @param fieldName field name coming from DynamoDB expression/condition * @param value comparison value */ public EqualsOperation(String fieldName, V 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 index e4dcc8c501..c042f63f10 100644 --- 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 @@ -10,7 +10,7 @@ public class GreaterThanEqualsOperation extends ComparisonOperation { /** * Creates a greater-than-or-equals comparison operation. * - * @param fieldName field name or path + * @param fieldName field name coming from DynamoDB expression/condition * @param value comparison value */ public GreaterThanEqualsOperation(String fieldName, V 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 index 35afe268c8..6ccdf43572 100644 --- 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 @@ -10,7 +10,7 @@ public class GreaterThanOperation extends ComparisonOperation { /** * Creates a greater-than comparison operation. * - * @param fieldName field name or path + * @param fieldName field name coming from DynamoDB expression/condition * @param value comparison value */ public GreaterThanOperation(String fieldName, V 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 index 310ea7276b..9ca900ae1d 100644 --- 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 @@ -10,7 +10,7 @@ public class LessThanEqualsOperation extends ComparisonOperation { /** * Creates a less-than-or-equals comparison operation. * - * @param fieldName field name or path + * @param fieldName field name coming from DynamoDB expression/condition * @param value comparison value */ public LessThanEqualsOperation(String fieldName, V 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 index ab4c16f6df..8288cbb170 100644 --- 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 @@ -10,7 +10,7 @@ public class LessThanOperation extends ComparisonOperation { /** * Creates a less-than comparison operation. * - * @param fieldName field name or path + * @param fieldName field name coming from DynamoDB expression/condition * @param value comparison value */ public LessThanOperation(String fieldName, V 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 index 9da15b5b7f..42eb51e4cb 100644 --- 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 @@ -10,7 +10,7 @@ public class NotEqualsOperation extends ComparisonOperation { /** * Creates an inequality comparison operation. * - * @param fieldName field name or path + * @param fieldName field name coming from DynamoDB expression/condition * @param value comparison value */ public NotEqualsOperation(String fieldName, V value) { From d55cbac9e51c9b613acdb6f389cd1c15c081cb0b Mon Sep 17 00:00:00 2001 From: aschenzle Date: Tue, 28 Apr 2026 11:57:42 -0700 Subject: [PATCH 4/7] Improving Javadoc --- .../java/controller/dynamodb/DynamoDbRequestParser.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 index 60d0576d3b..da5b583a37 100644 --- 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 @@ -32,8 +32,10 @@ public DynamoDbRequestParser() { /** * 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. + * @param request DynamoDB SDK request object + * @param apiMethodName DynamoDB API method name from enum type + * + * @return a map of parsed QueryOperations by table name. * Unsupported operations intentionally yield an empty map. */ public Map parseByTable(Object request, DynamoDbOperationNames apiMethodName) { From 77d5b8c1efea6d13f239642c2ac07c91951099e5 Mon Sep 17 00:00:00 2001 From: aschenzle Date: Wed, 29 Apr 2026 00:13:26 -0700 Subject: [PATCH 5/7] Added comments explainig decisions made around reflection. Added test to verify BETWEEN works with byte bounds (also ran an integration test but it's beyond the scope of this commit and will add as part of end 2 end tests later. Renamed all "Object request" to ddbRequest to make it easier to read. Improved Javadoc all around. --- .../dynamodb/DynamoDbRequestParser.java | 8 +- .../dynamodb/operations/BetweenOperation.java | 3 +- .../parsers/BatchGetItemApiMethodParser.java | 4 +- .../parsers/DynamoDbApiMethodParser.java | 4 +- .../parsers/DynamoDbBaseApiMethodParser.java | 42 +++--- .../parsers/GetItemApiMethodParser.java | 6 +- .../parsers/QueryApiMethodParser.java | 12 +- .../dynamodb/parsers/ScanApiMethodParser.java | 10 +- .../dynamodb/parsers/WriteMethodParser.java | 12 +- .../dynamodb/DynamoDbRequestParserTest.java | 28 ++++ .../java/instrumentation/DynamoDbCommand.java | 13 +- .../DynamoDbClassReplacement.java | 128 +++++++++++------- .../DynamoDbClassReplacementTest.java | 2 +- 13 files changed, 166 insertions(+), 106 deletions(-) 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 index da5b583a37..3d6b72e065 100644 --- 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 @@ -32,14 +32,14 @@ public DynamoDbRequestParser() { /** * Entry-point parser used by a future handler. - * @param request DynamoDB SDK request object + * @param ddbRequest DynamoDB SDK ddbRequest object (see comment on DynamoDbBaseApiMethodParser) * @param apiMethodName DynamoDB API method name from enum type * * @return a map of parsed QueryOperations by table name. * Unsupported operations intentionally yield an empty map. */ - public Map parseByTable(Object request, DynamoDbOperationNames apiMethodName) { - if (request == null || apiMethodName == null) { + public Map parseByTable(Object ddbRequest, DynamoDbOperationNames apiMethodName) { + if (ddbRequest == null || apiMethodName == null) { return Collections.emptyMap(); } @@ -48,7 +48,7 @@ public Map parseByTable(Object request, DynamoDbOperatio return Collections.emptyMap(); } - Map parsed = parser.parseRequest(request); + Map parsed = parser.parseRequest(ddbRequest); return parsed == null ? Collections.emptyMap() : parsed; } 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 index 3e60a9c4ed..b6962ad051 100644 --- 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 @@ -10,7 +10,8 @@ public class BetweenOperation extends QueryOperation { private final Object upperBound; /** - * Creates a BETWEEN operation. + * Creates a BETWEEN operation. Bounds are Objects because BETWEEN takes numbers, strings, and binary. + * Check docs at ... * * @param fieldName field name coming from DynamoDB expression/condition * @param lowerBound lower bound 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 index 70b12cbd3a..e0ca99e218 100644 --- 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 @@ -27,8 +27,8 @@ public DynamoDbOperationNames apiMethodName() { */ @Override @SuppressWarnings("unchecked") - public Map parseRequest(Object request) { - Object requestItemsObj = invokeNoArg(request, METHOD_REQUEST_ITEMS); + public Map parseRequest(Object ddbRequest) { + Object requestItemsObj = invokeNoArg(ddbRequest, METHOD_REQUEST_ITEMS); if (!(requestItemsObj instanceof Map)) { return Collections.emptyMap(); } 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 index be18a58124..2dcd1cc8fc 100644 --- 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 @@ -20,8 +20,8 @@ public interface DynamoDbApiMethodParser { /** * Parses one request object into table-specific query operations. * - * @param request DynamoDB request object + * @param ddbRequest DynamoDB request object * @return a map of parsed operations by table name */ - Map parseRequest(Object request); + Map parseRequest(Object ddbRequest); } 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 index 9637148444..bf5aa2fc05 100644 --- 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 @@ -13,6 +13,10 @@ /** * Base class for DynamoDB SDK requests parser. Contains shared utilities. + * DynamoDB requests are kept and parsed as the original SDK request objects because the typed request + * already preserves operation-specific fields and {@code AttributeValue} types, including binary data. + * Parsing that object directly avoids a lossy intermediate representation (like a string query) while + * still extracting only the table names and predicates needed by EvoMaster. @aschenzle */ abstract class DynamoDbBaseApiMethodParser implements DynamoDbApiMethodParser { @@ -43,13 +47,13 @@ protected QueryOperation parseExpression( } /** - * Parses key equality conditions from request key fields. + * Parses key equality conditions from DynamoDB request key fields. * - * @param request request object + * @param ddbRequest DynamoDB request object * @return parsed key condition operation */ - protected QueryOperation parseKeyCondition(Object request) { - Object keyObj = invokeNoArg(request, METHOD_KEY); + protected QueryOperation parseKeyCondition(Object ddbRequest) { + Object keyObj = invokeNoArg(ddbRequest, METHOD_KEY); return buildEqualsFromMap(DynamoDbAttributeValueHelper.toPlainMap(keyObj)); } @@ -115,13 +119,13 @@ protected QueryOperation combine(List conditions, Function readNameMap(Object request) { - Object raw = invokeNoArg(request, METHOD_EXPRESSION_ATTRIBUTE_NAMES); + protected Map readNameMap(Object ddbRequest) { + Object raw = invokeNoArg(ddbRequest, METHOD_EXPRESSION_ATTRIBUTE_NAMES); if (!(raw instanceof Map)) { return Collections.emptyMap(); } @@ -136,36 +140,36 @@ protected Map readNameMap(Object request) { } /** - * Reads and converts expression attribute values from request object. + * Reads and converts expression attribute values from DynamoDB request object. * - * @param request request object + * @param ddbRequest DynamoDB request object * @return normalized value map */ - protected Map readValueMap(Object request) { - Object raw = invokeNoArg(request, METHOD_EXPRESSION_ATTRIBUTE_VALUES); + protected Map readValueMap(Object ddbRequest) { + Object raw = invokeNoArg(ddbRequest, METHOD_EXPRESSION_ATTRIBUTE_VALUES); return DynamoDbAttributeValueHelper.toPlainMap(raw); } /** - * Reads a string-like value from request object via reflection. + * Reads a string-like value from a DDB request object via reflection. * - * @param target target object + * @param ddbRequest DynamoDB request object * @param methodName accessor method name * @return string value, or {@code null} */ - protected String readString(Object target, String methodName) { - Object value = invokeNoArg(target, methodName); + protected String readString(Object ddbRequest, String methodName) { + Object value = invokeNoArg(ddbRequest, methodName); return value == null ? null : String.valueOf(value); } /** * Reads and validates the table name from the request. * - * @param request request object + * @param ddbRequest DynamoDB request object * @return table name, or {@code null} if blank/absent */ - protected String readValidTableName(Object request) { - String tableName = readString(request, METHOD_TABLE_NAME); + protected String readValidTableName(Object ddbRequest) { + String tableName = readString(ddbRequest, METHOD_TABLE_NAME); if (tableName == null || tableName.trim().isEmpty()) { return null; } 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 index e6dac06531..077872d43e 100644 --- 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 @@ -22,9 +22,9 @@ public DynamoDbOperationNames apiMethodName() { * {@inheritDoc} */ @Override - public Map parseRequest(Object request) { - String tableName = readValidTableName(request); - QueryOperation keyCondition = parseKeyCondition(request); + public Map parseRequest(Object ddbRequest) { + String tableName = readValidTableName(ddbRequest); + QueryOperation keyCondition = parseKeyCondition(ddbRequest); return singleTableResult(tableName, keyCondition); } } 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 index 2003448fe9..4f231a5a62 100644 --- 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 @@ -23,22 +23,22 @@ public DynamoDbOperationNames apiMethodName() { * {@inheritDoc} */ @Override - public Map parseRequest(Object request) { - String tableName = readValidTableName(request); + public Map parseRequest(Object ddbRequest) { + String tableName = readValidTableName(ddbRequest); if (tableName == null) { return Collections.emptyMap(); } - Map names = readNameMap(request); - Map values = readValueMap(request); + Map names = readNameMap(ddbRequest); + Map values = readValueMap(ddbRequest); QueryOperation keyCondition = parseExpression( - readString(request, METHOD_KEY_CONDITION_EXPRESSION), + readString(ddbRequest, METHOD_KEY_CONDITION_EXPRESSION), names, values ); QueryOperation filterCondition = parseExpression( - readString(request, METHOD_FILTER_EXPRESSION), + readString(ddbRequest, METHOD_FILTER_EXPRESSION), names, values ); 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 index 9da73906b0..e6050d690b 100644 --- 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 @@ -23,16 +23,16 @@ public DynamoDbOperationNames apiMethodName() { * {@inheritDoc} */ @Override - public Map parseRequest(Object request) { - String tableName = readValidTableName(request); + public Map parseRequest(Object ddbRequest) { + String tableName = readValidTableName(ddbRequest); if (tableName == null) { return Collections.emptyMap(); } QueryOperation filterCondition = parseExpression( - readString(request, METHOD_FILTER_EXPRESSION), - readNameMap(request), - readValueMap(request) + readString(ddbRequest, METHOD_FILTER_EXPRESSION), + readNameMap(ddbRequest), + readValueMap(ddbRequest) ); return singleTableResult(tableName, filterCondition); 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 index 49bb00422d..7408240500 100644 --- 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 @@ -21,17 +21,17 @@ abstract class WriteMethodParser extends DynamoDbBaseApiMethodParser { * {@inheritDoc} */ @Override - public final Map parseRequest(Object request) { - String tableName = readValidTableName(request); + public final Map parseRequest(Object ddbRequest) { + String tableName = readValidTableName(ddbRequest); if (tableName == null) { return Collections.emptyMap(); } - QueryOperation keyCondition = requiresKeyCondition() ? parseKeyCondition(request) : null; + QueryOperation keyCondition = requiresKeyCondition() ? parseKeyCondition(ddbRequest) : null; QueryOperation conditionExpression = parseExpression( - readString(request, METHOD_CONDITION_EXPRESSION), - readNameMap(request), - readValueMap(request) + readString(ddbRequest, METHOD_CONDITION_EXPRESSION), + readNameMap(ddbRequest), + readValueMap(ddbRequest) ); return singleTableResult(tableName, combineWithAnd(keyCondition, conditionExpression)); 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 index 5ab993c983..abf2d38ee3 100644 --- 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 @@ -4,6 +4,7 @@ import org.evomaster.client.java.controller.dynamodb.operations.comparison.*; import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkBytes; import java.util.Arrays; import java.util.Collections; @@ -221,6 +222,33 @@ public void testUpdateItemCombinesKeyAndCondition() { assertEquals(41L, between.getUpperBound()); } + @Test + public void testUpdateItemParsesBetweenWithBinaryBounds() { + SdkBytes lowerBound = SdkBytes.fromByteArray(new byte[]{1, 10, 20}); + SdkBytes upperBound = SdkBytes.fromByteArray(new byte[]{1, 10, 90}); + + UpdateItemRequest request = UpdateItemRequest.builder() + .tableName("players") + .key(attributeValues("id", stringValue("messi-10"))) + .conditionExpression("#photo BETWEEN :lower AND :upper") + .expressionAttributeNames(names("#photo", "profilePhoto")) + .expressionAttributeValues(attributeValues( + ":lower", AttributeValue.builder().b(lowerBound).build(), + ":upper", AttributeValue.builder().b(upperBound).build() + )) + .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", "messi-10"); + BetweenOperation between = castAs(and.getConditions().get(1), BetweenOperation.class); + assertEquals("profilePhoto", between.getFieldName()); + assertArrayEquals(lowerBound.asByteArray(), ((SdkBytes) between.getLowerBound()).asByteArray()); + assertArrayEquals(upperBound.asByteArray(), ((SdkBytes) between.getUpperBound()).asByteArray()); + } + @Test public void testGetItemParsesCompositeKey() { GetItemRequest request = GetItemRequest.builder() 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 c7cf2cb9cd..378c1af54d 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 @@ -7,7 +7,8 @@ import java.util.Objects; /** - * Info related to DynamoDB command execution. + * Info related to DynamoDB command execution. We made the decision to store the whole DynamoDB request object. + * See DynamoDbBaseApiMethodParser for more details. */ public class DynamoDbCommand implements Serializable { @@ -23,7 +24,7 @@ public class DynamoDbCommand implements Serializable { /** * Actual executed operation */ - private final Object request; + private final Object ddbRequest; /** * If the operation was successfully executed */ @@ -33,12 +34,12 @@ public class DynamoDbCommand implements Serializable { */ private final long executionTime; - public DynamoDbCommand(List tableNames, DynamoDbOperationNames operationName, Object request, boolean successfullyExecuted, long executionTime) { + public DynamoDbCommand(List tableNames, DynamoDbOperationNames operationName, Object ddbRequest, boolean successfullyExecuted, long executionTime) { this.tableNames = tableNames == null ? Collections.emptyList() : Collections.unmodifiableList(new ArrayList<>(tableNames)); this.operationName = Objects.requireNonNull(operationName, "operationName cannot be null"); - this.request = request; + this.ddbRequest = ddbRequest; this.successfullyExecuted = successfullyExecuted; this.executionTime = executionTime; } @@ -51,8 +52,8 @@ public DynamoDbOperationNames getOperationName() { return operationName; } - public Object getRequest() { - return request; + public Object getDdbRequest() { + return ddbRequest; } public boolean isSuccessfullyExecuted() { 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 16a723df19..3066d319fc 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 @@ -23,7 +23,7 @@ /** * Instrumentation for DynamoDB client classes. * Made the decision to add both Sync and Async replacements in one class. - * They are mostly the same, but I couldn't find a better way with statics plus annotations + * They are mostly the same, but I couldn't find a better way with statics plus annotations limitations. */ public class DynamoDbClassReplacement { @@ -46,38 +46,38 @@ 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, DynamoDbOperationNames.GET_ITEM); + public static Object getItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.GetItemRequest") Object ddbRequest) { + return handle(client, DDB_GET_ITEM, ddbRequest, 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, DynamoDbOperationNames.BATCH_GET_ITEM); + public static Object batchGetItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest") Object ddbRequest) { + return handle(client, DDB_BATCH_GET_ITEM, ddbRequest, 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, DynamoDbOperationNames.PUT_ITEM); + public static Object putItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.PutItemRequest") Object ddbRequest) { + return handle(client, DDB_PUT_ITEM, ddbRequest, 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, DynamoDbOperationNames.UPDATE_ITEM); + public static Object updateItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest") Object ddbRequest) { + return handle(client, DDB_UPDATE_ITEM, ddbRequest, 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, DynamoDbOperationNames.DELETE_ITEM); + public static Object deleteItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest") Object ddbRequest) { + return handle(client, DDB_DELETE_ITEM, ddbRequest, 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, DynamoDbOperationNames.QUERY); + public static Object query(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.QueryRequest") Object ddbRequest) { + return handle(client, DDB_QUERY, ddbRequest, 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, DynamoDbOperationNames.SCAN); + public static Object scan(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.ScanRequest") Object ddbRequest) { + return handle(client, DDB_SCAN, ddbRequest, DynamoDbOperationNames.SCAN); } } @@ -97,54 +97,61 @@ 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, DynamoDbOperationNames.GET_ITEM); + public static Object getItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.GetItemRequest") Object ddbRequest) { + return handleAsync(client, DDB_ASYNC_GET_ITEM, ddbRequest, 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, DynamoDbOperationNames.BATCH_GET_ITEM); + public static Object batchGetItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest") Object ddbRequest) { + return handleAsync(client, DDB_ASYNC_BATCH_GET_ITEM, ddbRequest, 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, DynamoDbOperationNames.PUT_ITEM); + public static Object putItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.PutItemRequest") Object ddbRequest) { + return handleAsync(client, DDB_ASYNC_PUT_ITEM, ddbRequest, 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, DynamoDbOperationNames.UPDATE_ITEM); + public static Object updateItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest") Object ddbRequest) { + return handleAsync(client, DDB_ASYNC_UPDATE_ITEM, ddbRequest, 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, DynamoDbOperationNames.DELETE_ITEM); + public static Object deleteItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest") Object ddbRequest) { + return handleAsync(client, DDB_ASYNC_DELETE_ITEM, ddbRequest, 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, DynamoDbOperationNames.QUERY); + public static Object query(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.QueryRequest") Object ddbRequest) { + return handleAsync(client, DDB_ASYNC_QUERY, ddbRequest, 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, DynamoDbOperationNames.SCAN); + public static Object scan(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.ScanRequest") Object ddbRequest) { + return handleAsync(client, DDB_ASYNC_SCAN, ddbRequest, DynamoDbOperationNames.SCAN); } } /** * Invoke the original synchronous client method and trace the command execution. + * + * @param client DynamoDB Sync client object + * @param id unique identifier for Evomaster to match the replacement method with the original one. + * @param ddbRequest DynamoDB request object + * @param operationName enum of the DynamoDB operation (e.g., "GetItem", "PutItem", etc.) + * + * @return the result of the original method invocation. */ - protected static Object handle(Object client, String id, Object request, DynamoDbOperationNames operationName) { + protected static Object handle(Object client, String id, Object ddbRequest, DynamoDbOperationNames operationName) { long start = System.currentTimeMillis(); try { Method method = getOriginal(Sync.singleton, id, client); - Object result = method.invoke(client, request); + Object result = method.invoke(client, ddbRequest); long end = System.currentTimeMillis(); - List tableNames = extractTableNames(request); + List tableNames = extractTableNames(ddbRequest); long executionTime = end - start; - DynamoDbCommand info = new DynamoDbCommand(tableNames, operationName, request, true, executionTime); + DynamoDbCommand info = new DynamoDbCommand(tableNames, operationName, ddbRequest, true, executionTime); ExecutionTracer.addDynamoDbInfo(info); return result; } @@ -157,20 +164,27 @@ protected static Object handle(Object client, String id, Object request, DynamoD /** * Invoke the original asynchronous client method and trace completion status. + * + * @param client DynamoDB Async client object + * @param id unique identifier for Evomaster to match the replacement method with the original one. + * @param ddbRequest DynamoDB request object + * @param operationName enum of the DynamoDB operation (e.g., "GetItem", "PutItem", etc.) + * + * @return the result of the original method invocation. */ - protected static Object handleAsync(Object client, String id, Object request, DynamoDbOperationNames operationName) { + protected static Object handleAsync(Object client, String id, Object ddbRequest, DynamoDbOperationNames operationName) { long start = System.currentTimeMillis(); try { Method method = getOriginal(Async.singleton, id, client); - Object result = method.invoke(client, request); + Object result = method.invoke(client, ddbRequest); CompletableFuture future = (CompletableFuture) result; return future.handle((res, ex) -> { long end = System.currentTimeMillis(); - List tableNames = extractTableNames(request); + List tableNames = extractTableNames(ddbRequest); boolean successful = ex == null; long executionTime = end - start; - DynamoDbCommand info = new DynamoDbCommand(tableNames, operationName, request, successful, executionTime); + DynamoDbCommand info = new DynamoDbCommand(tableNames, operationName, ddbRequest, successful, executionTime); ExecutionTracer.addDynamoDbInfo(info); if (ex != null) { if (ex instanceof RuntimeException) throw (RuntimeException) ex; @@ -186,29 +200,38 @@ protected static Object handleAsync(Object client, String id, Object request, Dy } /** - * Extract table names from single-table and batch-table request types. + * Single entry point to extract table names from single-table and batch-table request types. + * + * @param ddbRequest DynamoDB request object + * + * @return a list containing a single table name (every operation except GetBatchItem) or list of table names, sorted alphabetically */ - private static List extractTableNames(Object request) { - if (request == null) return Collections.emptyList(); + private static List extractTableNames(Object ddbRequest) { + if (ddbRequest == null) return Collections.emptyList(); - //Assume it's GetItem first so try to extract single table name - String tableName = extractSingleTableName(request); + //Assume we need to extract just one table name (every operation except GetBatchItem) + String tableName = extractSingleTableName(ddbRequest); if (tableName != null) { return Collections.singletonList(tableName); } - return extractBatchTableNames(request); + return extractBatchTableNames(ddbRequest); } /** - * Extract table name from request objects that provide {@code tableName()}. + * Extract table name from DynamoDB request objects that provide {@value METHOD_TABLE_NAME}. + * Avoids throwing so {@code extractTableNames(Object)} can fall back to {@code extractBatchTableNames(Object)} + * + * @param ddbRequest DynamoDB request object + * + * @return a list containing a single table name (every operation except GetBatchItem) or {@code null} if reflection call fails. */ - private static String extractSingleTableName(Object request) { + private static String extractSingleTableName(Object ddbRequest) { try { - Method getTableNameMethod = request.getClass().getMethod(METHOD_TABLE_NAME); - return (String) getTableNameMethod.invoke(request); + Method getTableNameMethod = ddbRequest.getClass().getMethod(METHOD_TABLE_NAME); + return (String) getTableNameMethod.invoke(ddbRequest); } catch (NoSuchMethodException ignored) { - // Ignore as BatchGetItem requests do not have tableName and are handled by extractBatchTableNames. + // Fail gracefully so the caller can fall back return null; } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException("Failed to retrieve table name from DynamoDB request", e); @@ -217,11 +240,14 @@ private static String extractSingleTableName(Object request) { /** * Extract and sort table names from batchGetitem requests + * @param ddbRequest DynamoDB request object + * + * @return a list of table names, sorted alphabetically */ - private static List extractBatchTableNames(Object request) { + private static List extractBatchTableNames(Object ddbRequest) { try { - Method getRequestItemsMethod = request.getClass().getMethod(METHOD_REQUEST_ITEMS); - Object requestItems = getRequestItemsMethod.invoke(request); + Method getRequestItemsMethod = ddbRequest.getClass().getMethod(METHOD_REQUEST_ITEMS); + Object requestItems = getRequestItemsMethod.invoke(ddbRequest); if (!(requestItems instanceof Map)) { return Collections.emptyList(); } 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 cf0a79beb9..de80f3edc9 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 @@ -354,6 +354,6 @@ private void verifyInterception(List expectedTableNames, DynamoDbOperati DynamoDbCommand command = dynamoDbCommands.iterator().next(); assertEquals(expectedTableNames, command.getTableNames()); assertEquals(expectedOperationName, command.getOperationName()); - assertEquals(expectedRequest, command.getRequest()); + assertEquals(expectedRequest, command.getDdbRequest()); } } From ce8ee717f88e615c9db3929b7276b6dbf32c6ef9 Mon Sep 17 00:00:00 2001 From: aschenzle Date: Mon, 11 May 2026 22:49:32 -0700 Subject: [PATCH 6/7] Small fixes from PR comments --- client-java/controller/pom.xml | 4 ++-- .../dynamodb/parsers/BatchGetItemApiMethodParser.java | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client-java/controller/pom.xml b/client-java/controller/pom.xml index 211b1c7035..518cfedcc8 100644 --- a/client-java/controller/pom.xml +++ b/client-java/controller/pom.xml @@ -140,13 +140,13 @@ provided - + org.antlr antlr4-runtime + 4.9.3 - com.h2database h2 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 index e0ca99e218..d8f051326d 100644 --- 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 @@ -4,6 +4,7 @@ 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 org.evomaster.client.java.utils.SimpleLogger; import java.util.*; @@ -38,12 +39,14 @@ public Map parseRequest(Object ddbRequest) { for (Map.Entry entry : requestItems.entrySet()) { String tableName = entry.getKey() == null ? null : String.valueOf(entry.getKey()); if (tableName == null || tableName.trim().isEmpty()) { + SimpleLogger.uniqueWarn("Failed to parse tablename for BatchGetItem"); continue; } Object keysAndAttributes = entry.getValue(); Object keysObj = invokeNoArg(keysAndAttributes, METHOD_KEYS); if (!(keysObj instanceof Collection)) { + SimpleLogger.uniqueWarn("Failed to parse keys for BatchGetItem for table: " + tableName); continue; } From 11436650b17979ebd0f6274777deda18e54b84c7 Mon Sep 17 00:00:00 2001 From: aschenzle Date: Thu, 14 May 2026 13:51:03 -0700 Subject: [PATCH 7/7] Small fixes from PR comments --- client-java/controller/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-java/controller/pom.xml b/client-java/controller/pom.xml index 518cfedcc8..51fdb0f621 100644 --- a/client-java/controller/pom.xml +++ b/client-java/controller/pom.xml @@ -216,7 +216,7 @@ dynamodb test - + org.slf4j slf4j-simple