Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package org.evomaster.client.java.controller.dynamodb;

import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto;
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto;
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionResultsDto;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletionStage;

/**
* Executes DynamoDB insertions without binding the controller API to an AWS SDK version.
*/
public final class DynamoDbCommandExecutor {

private static final String ATTRIBUTE_VALUE_CLASS_NAME =
"software.amazon.awssdk.services.dynamodb.model.AttributeValue";
private static final String ATTRIBUTE_VALUE_BUILDER_CLASS_NAME = ATTRIBUTE_VALUE_CLASS_NAME + "$Builder";
private static final String PUT_ITEM_REQUEST_CLASS_NAME =
"software.amazon.awssdk.services.dynamodb.model.PutItemRequest";
private static final String PUT_ITEM_REQUEST_BUILDER_CLASS_NAME = PUT_ITEM_REQUEST_CLASS_NAME + "$Builder";
private static final String DYNAMODB_CLIENT_CLASS_NAME =
"software.amazon.awssdk.services.dynamodb.DynamoDbClient";
private static final String DYNAMODB_ASYNC_CLIENT_CLASS_NAME =
"software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient";

private static final String BUILDER_METHOD_NAME = "builder";
private static final String BUILD_METHOD_NAME = "build";
private static final String TABLE_NAME_METHOD_NAME = "tableName";
private static final String ITEM_METHOD_NAME = "item";
private static final String PUT_ITEM_METHOD_NAME = "putItem";
private static final String STRING_VALUE_METHOD_NAME = "s";
private static final String NUMBER_VALUE_METHOD_NAME = "n";
private static final String BOOLEAN_VALUE_METHOD_NAME = "bool";

/**
* Prevents instantiation of this utility class.
*/
private DynamoDbCommandExecutor() {
}

/**
* Executes insertions using a synchronous or asynchronous AWS SDK v2 client.
*
* @param client DynamoDB client
* @param insertions items to insert
* @return per-insertion results, stopping at the first failed insertion
* @throws NullPointerException when the client or insertion list is {@code null}
* @throws IllegalArgumentException when the insertion list is empty
*/
public static DynamoDbInsertionResultsDto executeInsert(Object client, List<DynamoDbInsertionDto> insertions) {
Objects.requireNonNull(client, "DynamoDB client cannot be null");
Objects.requireNonNull(insertions, "DynamoDB insertions cannot be null");
if (insertions.isEmpty()) {
throw new IllegalArgumentException("No data to insert");
}

DynamoDbInsertionResultsDto results = new DynamoDbInsertionResultsDto();
results.executionResults = new ArrayList<>(Collections.nCopies(insertions.size(), false));
for (int i = 0; i < insertions.size(); i++) {
try {
executeOne(client, insertions.get(i));
results.executionResults.set(i, true);
} catch (RuntimeException ignored) {
results.failedInsertionIndex = i;
return results;
}
}
return results;
}

/**
* Executes one DynamoDB insertion through the AWS SDK v2 reflection API.
*
* @param client synchronous or asynchronous DynamoDB client
* @param insertion item to insert
*/
private static void executeOne(Object client, DynamoDbInsertionDto insertion) {
try {
ClassLoader loader = client.getClass().getClassLoader();
Class<?> attributeValueClass = Class.forName(
ATTRIBUTE_VALUE_CLASS_NAME, true, loader);
Class<?> attributeValueBuilderClass = Class.forName(
ATTRIBUTE_VALUE_BUILDER_CLASS_NAME, true, loader);
Class<?> putItemRequestClass = Class.forName(
PUT_ITEM_REQUEST_CLASS_NAME, true, loader);
Class<?> putItemRequestBuilderClass = Class.forName(
PUT_ITEM_REQUEST_BUILDER_CLASS_NAME, true, loader);

Map<String, Object> item = new LinkedHashMap<>();
for (DynamoDbAttributeValueDto attribute : insertion.attributes) {
Object builder = attributeValueClass.getMethod(BUILDER_METHOD_NAME).invoke(null);
String setter;
Object value = attribute.value;
switch (attribute.type) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not having the same d action for all printable values?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there's a confusion. Here we determine which method to call by reflection in line 115. We need to use the exact method name (s,n,bool).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

case STRING:
setter = STRING_VALUE_METHOD_NAME;
break;
case NUMBER:
setter = NUMBER_VALUE_METHOD_NAME;
break;
case BOOLEAN:
setter = BOOLEAN_VALUE_METHOD_NAME;
value = Boolean.valueOf(attribute.value);
break;
default:
throw new IllegalArgumentException("Unsupported DynamoDB attribute type: " + attribute.type);
}
attributeValueBuilderClass.getMethod(setter, value.getClass()).invoke(builder, value);
item.put(attribute.attributeName,
attributeValueBuilderClass.getMethod(BUILD_METHOD_NAME).invoke(builder));
}

Object requestBuilder = putItemRequestClass.getMethod(BUILDER_METHOD_NAME).invoke(null);
putItemRequestBuilderClass.getMethod(TABLE_NAME_METHOD_NAME, String.class)
.invoke(requestBuilder, insertion.tableName);
putItemRequestBuilderClass.getMethod(ITEM_METHOD_NAME, Map.class).invoke(requestBuilder, item);
Object request = putItemRequestBuilderClass.getMethod(BUILD_METHOD_NAME).invoke(requestBuilder);
Method putItem = findPutItemMethod(client, loader, putItemRequestClass);
Object response = putItem.invoke(client, request);
if (response instanceof CompletionStage) {
((CompletionStage<?>) response).toCompletableFuture().join();
}
} catch (InvocationTargetException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
throw new RuntimeException("Failed DynamoDB insertion into table '" + insertion.tableName + "'", cause);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed DynamoDB insertion into table '" + insertion.tableName + "'", e);
}
}

/**
* Finds the insertion method exposed by a synchronous or asynchronous AWS SDK v2 client.
*
* @param client DynamoDB client
* @param loader client class loader
* @param putItemRequestClass reflected request class
* @return reflected {@code putItem} method
* @throws ClassNotFoundException when the AWS client types are unavailable
* @throws NoSuchMethodException when the client does not expose the expected method
*/
private static Method findPutItemMethod(Object client, ClassLoader loader, Class<?> putItemRequestClass)
throws ClassNotFoundException, NoSuchMethodException {
Class<?> syncClientClass = Class.forName(DYNAMODB_CLIENT_CLASS_NAME, true, loader);
if (syncClientClass.isInstance(client)) {
return syncClientClass.getMethod(PUT_ITEM_METHOD_NAME, putItemRequestClass);
}

Class<?> asyncClientClass = Class.forName(DYNAMODB_ASYNC_CLIENT_CLASS_NAME, true, loader);
if (asyncClientClass.isInstance(client)) {
return asyncClientClass.getMethod(PUT_ITEM_METHOD_NAME, putItemRequestClass);
}

throw new IllegalArgumentException("Unsupported DynamoDB client: " + client.getClass().getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.evomaster.client.java.controller.dynamodb;

import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto;
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto;
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionResultsDto;
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.PutItemResponse;

import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/** Tests DynamoDB item insertion through synchronous and asynchronous AWS clients. */
public class DynamoDbCommandExecutorTest {

@Test
public void testExecuteInsertWithSynchronousClient() {
DynamoDbClient client = mock(DynamoDbClient.class);
when(client.putItem(any(PutItemRequest.class))).thenReturn(PutItemResponse.builder().build());

DynamoDbInsertionResultsDto results = DynamoDbCommandExecutor.executeInsert(
client, Collections.singletonList(worldCupPlayer()));

ArgumentCaptor<PutItemRequest> requestCaptor = ArgumentCaptor.forClass(PutItemRequest.class);
verify(client).putItem(requestCaptor.capture());
PutItemRequest request = requestCaptor.getValue();
assertEquals("WorldCupPlayers", request.tableName());
assertEquals("Argentina", request.item().get("country").s());
assertEquals("10", request.item().get("fifaId").n());
assertTrue(request.item().get("captain").bool());
assertEquals(Collections.singletonList(true), results.executionResults);
assertNull(results.failedInsertionIndex);
}

@Test
public void testExecuteInsertWithAsynchronousClient() {
DynamoDbAsyncClient client = mock(DynamoDbAsyncClient.class);
when(client.putItem(any(PutItemRequest.class))).thenReturn(
CompletableFuture.completedFuture(PutItemResponse.builder().build()));

DynamoDbInsertionResultsDto results = DynamoDbCommandExecutor.executeInsert(
client, Collections.singletonList(worldCupPlayer()));

verify(client).putItem(any(PutItemRequest.class));
assertEquals(Collections.singletonList(true), results.executionResults);
}

@Test
public void testFailureReturnsPartialResults() {
DynamoDbClient client = mock(DynamoDbClient.class);
when(client.putItem(any(PutItemRequest.class)))
.thenReturn(PutItemResponse.builder().build())
.thenThrow(new IllegalStateException("DynamoDB unavailable"));

DynamoDbInsertionResultsDto results = DynamoDbCommandExecutor.executeInsert(
client, Arrays.asList(worldCupPlayer(), worldCupPlayer(), worldCupPlayer()));

assertEquals(Arrays.asList(true, false, false), results.executionResults);
assertEquals(Integer.valueOf(1), results.failedInsertionIndex);
verify(client, times(2)).putItem(any(PutItemRequest.class));
}

@Test
public void testRejectsMissingClientOrInsertions() {
DynamoDbClient client = mock(DynamoDbClient.class);

assertThrows(NullPointerException.class,
() -> DynamoDbCommandExecutor.executeInsert(null, Collections.singletonList(worldCupPlayer())));
assertThrows(NullPointerException.class,
() -> DynamoDbCommandExecutor.executeInsert(client, null));
assertThrows(IllegalArgumentException.class,
() -> DynamoDbCommandExecutor.executeInsert(client, Collections.emptyList()));
verify(client, times(0)).putItem(any(PutItemRequest.class));
}

private DynamoDbInsertionDto worldCupPlayer() {
DynamoDbInsertionDto insertion = new DynamoDbInsertionDto();
insertion.tableName = "WorldCupPlayers";
insertion.attributes.add(new DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.STRING, "Argentina"));
insertion.attributes.add(new DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.NUMBER, "10"));
insertion.attributes.add(new DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOLEAN, "true"));
return insertion;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.evomaster.core.database.dynamodb

import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto
import org.evomaster.core.search.action.Action
import org.evomaster.core.search.action.EnvironmentAction
import org.evomaster.core.search.gene.Gene

/**
* A typed attribute gene belonging to a DynamoDB item.
*
* @property attributeName name of the DynamoDB item attribute
* @property type supported DynamoDB scalar type
* @property gene evolvable value for the attribute
*/
data class DynamoDbAttributeGene(
val attributeName: String,
val type: DynamoDbScalarTypeDto,
val gene: Gene
)

/**
* An initialization action that inserts one DynamoDB item.
*
* @property tableName target DynamoDB table
* @property attributes item attributes to insert
*/
class DynamoDbAction(
val tableName: String,
val attributes: List<DynamoDbAttributeGene>
) : EnvironmentAction(listOf()) {

companion object {
private const val ATTRIBUTE_SEPARATOR = '|'
private const val TYPE_SEPARATOR = ':'
private const val VALUE_SEPARATOR = '='
}

init {
addChildren(attributes.map { it.gene })
}

/** Returns the genes that determine the inserted item values. */
override fun seeTopGenes(): List<Gene> = attributes.map { it.gene }

/** Creates an independent action with copies of all attribute genes. */
override fun copyContent(): Action = DynamoDbAction(
tableName,
attributes.map { DynamoDbAttributeGene(it.attributeName, it.type, it.gene.copy()) }
)

/** Returns the descriptive name of this insertion action. */
override fun getName(): String = "DynamoDB_INSERT_$tableName"

/** Returns the grouping key for DynamoDB initialization actions. */
override fun getActionGroupKey(): String = DynamoDbAction::class.java.name

/** Stable key used to avoid adding the same inferred insertion twice. */
fun insertionKey(): String = buildString {
append(tableName)
attributes.forEach {
append(ATTRIBUTE_SEPARATOR).append(it.attributeName).append(TYPE_SEPARATOR).append(it.type)
.append(VALUE_SEPARATOR).append(it.gene.getValueAsRawString())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.evomaster.core.database.dynamodb

import org.evomaster.core.search.action.Action
import org.evomaster.core.search.action.ActionResult

/** Result of executing a [DynamoDbAction]. */
class DynamoDbActionResult : ActionResult {

/** Creates a result for the action identified by [sourceLocalId]. */
constructor(sourceLocalId: String, stopping: Boolean = false) : super(sourceLocalId, stopping)

/** Creates a copy of another DynamoDB action result. */
constructor(other: DynamoDbActionResult) : super(other)

companion object {
const val INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY = "INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY"
}

/** Creates an independent copy of this result. */
override fun copy(): DynamoDbActionResult = DynamoDbActionResult(this)

/** Records whether the insertion completed successfully. */
fun setInsertExecutionResult(success: Boolean) =
addResultValue(INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY, success.toString())

/** Returns whether the insertion completed successfully. */
fun getInsertExecutionResult(): Boolean =
getResultValue(INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY)?.toBoolean() ?: false

/** Returns whether [action] is a DynamoDB insertion action. */
override fun matchedType(action: Action): Boolean = action is DynamoDbAction
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.evomaster.core.database.dynamodb

import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbDatabaseCommandsDto
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto
import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto
import org.evomaster.core.search.gene.BooleanGene
import org.evomaster.core.search.gene.numeric.BigDecimalGene
import org.evomaster.core.search.gene.string.StringGene

/** Transforms DynamoDB actions into controller insertion commands. */
object DynamoDbActionTransformer {

/** Converts initialization actions to the controller's DynamoDB insertion DTO. */
fun transform(actions: List<DynamoDbAction>): DynamoDbDatabaseCommandsDto =
DynamoDbDatabaseCommandsDto().also { commands ->
commands.insertions = actions.map { action ->
DynamoDbInsertionDto().also { insertion ->
insertion.tableName = action.tableName
insertion.attributes = action.attributes.map { attribute ->
DynamoDbAttributeValueDto(
attribute.attributeName,
attribute.type,
when (attribute.type) {
DynamoDbScalarTypeDto.STRING -> (attribute.gene as StringGene).value
DynamoDbScalarTypeDto.NUMBER -> (attribute.gene as BigDecimalGene).value.toPlainString()
DynamoDbScalarTypeDto.BOOLEAN -> (attribute.gene as BooleanGene).value.toString()
}
)
}
}
}
}
}
Loading
Loading