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
5 changes: 5 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@
<groupId>com.google.classpath-explorer</groupId>
<artifactId>classpath-explorer</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.500</version>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public void configure() {
bind(ZeroCodeExternalFileProcessor.class).to(ZeroCodeExternalFileProcessorImpl.class);
bind(ZeroCodeParameterizedProcessor.class).to(ZeroCodeParameterizedProcessorImpl.class);
bind(ZeroCodeSorter.class).to(ZeroCodeSorterImpl.class);
bind(org.jsmart.zerocode.core.s3.S3Client.class).to(org.jsmart.zerocode.core.s3.BasicS3Client.class);

// ------------------------------------------------
// Bind properties for localhost, CI, DIT, SIT etc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,14 @@ public interface ApiServiceExecutor {
*/
String executeKafkaService(String kafkaServers, String kafkaTopic, String methodName, String requestJson, ScenarioExecutionState scenarioExecutionState);

/**
*
* @param bucketName The name of the S3 bucket extracted from the url
* @param operation An S3 operation e.g. upload, download, list
* @param requestJson A json with s3 parameters
* @param scenarioExecutionState The state of the scenario execution
* @return String The S3 operation result in JSON
*/
String executeS3Service(String bucketName, String operation, String requestJson, ScenarioExecutionState scenarioExecutionState);

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public class ApiServiceExecutorImpl implements ApiServiceExecutor {
@Inject
private BasicKafkaClient kafkaClient;

@Inject
private org.jsmart.zerocode.core.s3.S3Client s3Client;

@Inject(optional = true)
@Named("mock.api.port")
private int mockPort;
Expand Down Expand Up @@ -58,4 +61,9 @@ public String executeJavaOperation(String className, String methodName, String r
public String executeKafkaService(String kafkaServers, String kafkaTopic, String operation, String requestJson, ScenarioExecutionState scenarioExecutionState) {
return kafkaClient.execute(kafkaServers, kafkaTopic, operation, requestJson, scenarioExecutionState);
}

@Override
public String executeS3Service(String bucketName, String operation, String requestJson, ScenarioExecutionState scenarioExecutionState) {
return s3Client.execute(bucketName, operation, requestJson, scenarioExecutionState);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,20 @@ private String executeApi(String logPrefixRelationshipId,
executionResult = apiExecutor.executeKafkaService(kafkaServers, topicName, operationName, resolvedRequestJsonMaskRemoved, scenarioExecutionState);
break;

case S3_CALL:
correlLogger.aRequestBuilder()
.relationshipId(logPrefixRelationshipId)
.requestTimeStamp(requestTimeStamp)
.step(thisStepName)
.url(url)
.method(operationName)
.id(stepId)
.request(prettyPrintJson(resolvedRequestJsonMaskApplied));

String bucketName = url.substring("s3-bucket:".length());
executionResult = apiExecutor.executeS3Service(bucketName, operationName, resolvedRequestJsonMaskRemoved, scenarioExecutionState);
break;

case NONE:
correlLogger.aRequestBuilder()
.relationshipId(logPrefixRelationshipId)
Expand Down
236 changes: 236 additions & 0 deletions core/src/main/java/org/jsmart/zerocode/core/s3/BasicS3Client.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package org.jsmart.zerocode.core.s3;

import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.BasicSessionCredentials;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.ListObjectsV2Request;
import com.amazonaws.services.s3.model.ListObjectsV2Result;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import org.apache.commons.lang3.StringUtils;
import org.jsmart.zerocode.core.engine.preprocessor.ScenarioExecutionState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BasicS3Client implements S3Client {
private static final Logger LOGGER = LoggerFactory.getLogger(BasicS3Client.class);
private static final String FAILED = "Failed";

@Inject
private ObjectMapper objectMapper;

@Inject(optional = true)
@Named("s3.accessKey")
private String s3AccessKey;

@Inject(optional = true)
@Named("s3.secretKey")
private String s3SecretKey;

@Inject(optional = true)
@Named("s3.region")
private String s3Region;

@Inject(optional = true)
@Named("s3.token")
private String s3Token;

@Inject(optional = true)
@Named("s3.endpoint")
private String s3Endpoint;

public BasicS3Client() {
}

private AmazonS3 buildS3Client() {
AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard();

String envAccessKey = System.getenv("AWS_ACCESS_KEY_ID");
String envSecretKey = System.getenv("AWS_SECRET_ACCESS_KEY");
String envSessionToken = System.getenv("AWS_SESSION_TOKEN");
String envRegion = System.getenv("AWS_REGION");

String finalAccessKey = StringUtils.isNotBlank(envAccessKey) ? envAccessKey : s3AccessKey;
String finalSecretKey = StringUtils.isNotBlank(envSecretKey) ? envSecretKey : s3SecretKey;
String finalToken = StringUtils.isNotBlank(envSessionToken) ? envSessionToken : s3Token;
String finalRegion = StringUtils.isNotBlank(envRegion) ? envRegion : s3Region;

if (StringUtils.isNotBlank(finalAccessKey) && StringUtils.isNotBlank(finalSecretKey)) {
AWSCredentialsProvider credentialsProvider;
if (StringUtils.isNotBlank(finalToken)) {
credentialsProvider = new AWSStaticCredentialsProvider(new BasicSessionCredentials(finalAccessKey, finalSecretKey, finalToken));
} else {
credentialsProvider = new AWSStaticCredentialsProvider(new BasicAWSCredentials(finalAccessKey, finalSecretKey));
}
builder.withCredentials(credentialsProvider);
} else {
// fallback to default provider chain
builder.withCredentials(new DefaultAWSCredentialsProviderChain());
}

if (StringUtils.isNotBlank(s3Endpoint)) {
builder.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3Endpoint, finalRegion));
} else if (StringUtils.isNotBlank(finalRegion)) {
builder.withRegion(finalRegion);
}

return builder.build();
}

@Override
public String execute(String bucketName, String operation, String requestJson, ScenarioExecutionState scenarioExecutionState) {
LOGGER.debug("S3 operation: {}, bucket: {}, request: {}", operation, bucketName, requestJson);

try {
AmazonS3 s3Client = buildS3Client();

String actionName = operation;
if (actionName.contains(".")) {
actionName = actionName.substring(actionName.indexOf('.') + 1);
}
S3Action s3Action = S3Action.fromString(actionName);

JsonNode requestNode = null;
if (StringUtils.isNotBlank(requestJson)) {
requestNode = objectMapper.readTree(requestJson);
}

switch (s3Action) {
case UPLOAD:
return handleUpload(s3Client, bucketName, requestNode);
case DOWNLOAD:
return handleDownload(s3Client, bucketName, requestNode);
case LIST:
return handleList(s3Client, bucketName, requestNode);
default:
throw new RuntimeException("Unsupported S3 operation: " + operation);
}
} catch (Throwable e) {
LOGGER.error("Error executing S3 operation: {}", e.getMessage(), e);
try {
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("status", FAILED);
errorResponse.put("message", e.getMessage());
return objectMapper.writeValueAsString(errorResponse);
} catch (JsonProcessingException ex) {
throw new RuntimeException(e);
}
}
}

private String handleUpload(AmazonS3 s3Client, String bucket, JsonNode requestNode) throws JsonProcessingException {
validateRequestNode(requestNode);
String key = getRequiredString(requestNode, "key");
String filePath = getRequiredString(requestNode, "filePath");

File file = validateAndGetFile(filePath);

s3Client.putObject(bucket, key, file);

Map<String, Object> result = new HashMap<>();
result.put("status", 200);
result.put("bucket", bucket);
result.put("key", key);

return objectMapper.writeValueAsString(result);
}

private String handleDownload(AmazonS3 s3Client, String bucket, JsonNode requestNode) throws JsonProcessingException {
validateRequestNode(requestNode);
String key = getRequiredString(requestNode, "key");
String saveAs = getRequiredString(requestNode, "saveAs");

File outFile = new File(saveAs);
if (!outFile.isAbsolute() && !saveAs.startsWith("/")) {
outFile = new File("target/" + saveAs);
}

File parent = outFile.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}

s3Client.getObject(new com.amazonaws.services.s3.model.GetObjectRequest(bucket, key), outFile);

Map<String, Object> result = new HashMap<>();
result.put("downloaded", true);
result.put("savedTo", outFile.getAbsolutePath());

return objectMapper.writeValueAsString(result);
}

private String handleList(AmazonS3 s3Client, String bucket, JsonNode requestNode) throws JsonProcessingException {
String folder = null;
if (requestNode != null && requestNode.has("folder") && !requestNode.get("folder").isNull()) {
folder = requestNode.get("folder").asText();
}

ListObjectsV2Request req = new ListObjectsV2Request().withBucketName(bucket);
if (StringUtils.isNotBlank(folder)) {
req.withPrefix(folder);
}

ListObjectsV2Result objectListing = s3Client.listObjectsV2(req);
List<Map<String, Object>> files = new ArrayList<>();

for (S3ObjectSummary summary : objectListing.getObjectSummaries()) {
Map<String, Object> fileInfo = new HashMap<>();
fileInfo.put("key", summary.getKey());
fileInfo.put("size", summary.getSize());
fileInfo.put("lastModified", summary.getLastModified().getTime());
files.add(fileInfo);
}

Map<String, Object> result = new HashMap<>();
result.put("files", files);
result.put("files.SIZE", files.size());

return objectMapper.writeValueAsString(result);
}

private void validateRequestNode(JsonNode requestNode) {
if (requestNode == null) {
throw new IllegalArgumentException("Missing required request body for S3 operation.");
}
}

private String getRequiredString(JsonNode node, String fieldName) {
if (!node.has(fieldName) || node.get(fieldName).isNull()) {
throw new IllegalArgumentException("Missing required field: " + fieldName);
}
return node.get(fieldName).asText();
}

private File validateAndGetFile(String fileName) {
File absoluteFile = new File(fileName);
if (absoluteFile.exists()) {
return absoluteFile;
}

try {
URL resource = getClass().getClassLoader().getResource(fileName);
if (resource == null) {
throw new IllegalArgumentException("File does not exist: " + fileName);
}
return new File(resource.getFile());
} catch (Exception ex) {
throw new RuntimeException("Error accessing file: `" + fileName + "' - " + ex);
}
}
}
26 changes: 26 additions & 0 deletions core/src/main/java/org/jsmart/zerocode/core/s3/S3Action.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.jsmart.zerocode.core.s3;

public enum S3Action {
UPLOAD("UPLOAD"),
DOWNLOAD("DOWNLOAD"),
LIST("LIST");

private final String action;

S3Action(String action) {
this.action = action;
}

public String getAction() {
return action;
}

public static S3Action fromString(String action) {
for (S3Action s3Action : values()) {
if (s3Action.action.equalsIgnoreCase(action)) {
return s3Action;
}
}
throw new IllegalArgumentException("Unknown S3 action: " + action);
}
}
7 changes: 7 additions & 0 deletions core/src/main/java/org/jsmart/zerocode/core/s3/S3Client.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.jsmart.zerocode.core.s3;

import org.jsmart.zerocode.core.engine.preprocessor.ScenarioExecutionState;

public interface S3Client {
String execute(String bucketName, String operation, String requestJson, ScenarioExecutionState scenarioExecutionState);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public enum ApiType {
REST_CALL,
KAFKA_CALL,
S3_CALL,
JAVA_CALL,
NONE;
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public static ApiType apiType(String serviceName, String methodName) {
} else if (serviceName != null && serviceName.contains(KAFKA)) {
apiType = ApiType.KAFKA_CALL;

} else if (serviceName != null && serviceName.startsWith("s3-bucket:")) {
apiType = ApiType.S3_CALL;

} else {
apiType = ApiType.JAVA_CALL;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.jsmart.zerocode.core.s3;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.jsmart.zerocode.core.di.main.ApplicationMainModule;
import org.jsmart.zerocode.core.engine.preprocessor.ScenarioExecutionState;
import org.junit.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;

public class BasicS3ClientTest {

@Test
public void testS3ActionParsing() {
S3Action upload = S3Action.fromString("UPLOAD");
assertThat(upload, is(S3Action.UPLOAD));

S3Action list = S3Action.fromString("list");
assertThat(list, is(S3Action.LIST));
}

@Test(expected = IllegalArgumentException.class)
public void testS3ActionParsing_Invalid() {
S3Action.fromString("INVALID");
}
}
Loading
Loading