diff --git a/.gitignore b/.gitignore
index d9c73d8..856e984 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ unicorn_approvals/ApprovalsService/target/**
unicorn_contracts/ContractsService/target/**
unicorn_web/ApprovalService/target/**
unicorn_web/SearchService/target/**
+unicorn_web/PublicationManagerService/target/**
unicorn_web/Common/target/**
**/.aws-sam/
.DS_Store**
diff --git a/unicorn_approvals/ApprovalsService/pom.xml b/unicorn_approvals/ApprovalsService/pom.xml
index 0fd849c..24f69f3 100644
--- a/unicorn_approvals/ApprovalsService/pom.xml
+++ b/unicorn_approvals/ApprovalsService/pom.xml
@@ -9,14 +9,14 @@
17
17
- 2.27.21
- 1.18.0
- 3.13.0
- 5.13.0
+ 2.32.29
+ 1.20.2
+ 3.16.1
+ 5.18.0
4.13.2
- 1.1.1
- 1.2.3
- 2.27.21
+ 1.1.2
+ 1.3.0
+ 2.32.29
@@ -81,29 +81,29 @@
com.fasterxml.jackson.core
jackson-databind
- 2.15.2
+ 2.18.4
com.fasterxml.jackson.core
jackson-core
- 2.15.2
+ 2.18.4
com.fasterxml.jackson.core
jackson-annotations
- 2.15.2
+ 2.18.4
org.apache.logging.log4j
log4j-api
- 2.20.0
+ 2.25.1
org.apache.logging.log4j
log4j-core
- 2.20.0
+ 2.25.1
org.mockito
@@ -132,7 +132,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 2.22.2
+ 3.5.3
handler
@@ -142,7 +142,7 @@
org.apache.maven.plugins
maven-shade-plugin
- 3.2.4
+ 3.6.0
@@ -157,11 +157,10 @@
dev.aspectj
aspectj-maven-plugin
- 1.13.1
+ 1.14.1
- 17
- 17
17
+ 17
software.amazon.lambda
@@ -184,11 +183,18 @@
+
+
+ org.aspectj
+ aspectjtools
+ 1.9.24
+
+
org.apache.maven.plugins
maven-compiler-plugin
- 3.11.0
+ 3.14.0
17
17
diff --git a/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java
index 7358dc0..0a82802 100644
--- a/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java
+++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java
@@ -27,70 +27,60 @@
*/
public class ContractStatusChangedHandlerFunction {
- Logger logger = LogManager.getLogger();
-
- final String TABLE_NAME = System.getenv("CONTRACT_STATUS_TABLE");
-
- ObjectMapper objectMapper = new ObjectMapper();
-
- DynamoDbClient dynamodbClient = DynamoDbClient.builder()
- .build();
-
- /**
- *
- * @param inputStream
- * @param outputStream
- * @param context
- * @return
- * @throws IOException
- *
- */
- @Tracing
- @Metrics(captureColdStart = true)
- @Logging(logEvent = true)
- public void handleRequest(InputStream inputStream, OutputStream outputStream,
- Context context) throws IOException {
-
- // deseralised and save contract status change in dynamodb table
-
- Event event = Marshaller.unmarshal(inputStream,
- Event.class);
- // save to database
- ContractStatusChanged contractStatusChanged = event.getDetail();
- saveContractStatus(contractStatusChanged.getPropertyId(), contractStatusChanged.getContractStatus(),
- contractStatusChanged.getContractId(),
- contractStatusChanged.getContractLastModifiedOn());
-
- OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
- writer.write(objectMapper.writeValueAsString(event.getDetail()));
- writer.close();
- }
-
- @Tracing
- void saveContractStatus(String propertyId,
- String contractStatus, String contractId, Long contractLastModifiedOn) {
- Map key = new HashMap();
- AttributeValue keyvalue = AttributeValue.fromS(propertyId);
- key.put("property_id", keyvalue);
-
- Map expressionAttributeValues = new HashMap();
- expressionAttributeValues.put(":t", AttributeValue.fromS(contractStatus));
- expressionAttributeValues.put(":c", AttributeValue.fromS(contractId));
- expressionAttributeValues.put(":m", AttributeValue
- .fromN(String.valueOf(contractLastModifiedOn)));
-
- UpdateItemRequest updateItemRequest = UpdateItemRequest.builder()
- .key(key)
- .tableName(TABLE_NAME)
- .updateExpression(
- "set contract_status=:t, contract_last_modified_on=:m, contract_id=:c")
- .expressionAttributeValues(expressionAttributeValues)
- .build();
-
- dynamodbClient.updateItem(updateItemRequest);
- }
-
- public void setDynamodbClient(DynamoDbClient dynamodbClient) {
- this.dynamodbClient = dynamodbClient;
+ private static final Logger logger = LogManager.getLogger();
+ private static final String TABLE_NAME = System.getenv("CONTRACT_STATUS_TABLE");
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ private DynamoDbClient dynamodbClient = DynamoDbClient.builder().build();
+
+ /**
+ * Handles contract status change events from EventBridge
+ *
+ * @param inputStream the input stream containing the event
+ * @param outputStream the output stream for the response
+ * @param context the Lambda context
+ * @throws IOException if there's an error processing the event
+ */
+ @Tracing
+ @Metrics(captureColdStart = true)
+ @Logging(logEvent = true)
+ public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
+ Event event = Marshaller.unmarshal(inputStream, Event.class);
+ ContractStatusChanged contractStatusChanged = event.getDetail();
+
+ saveContractStatus(
+ contractStatusChanged.getPropertyId(),
+ contractStatusChanged.getContractStatus(),
+ contractStatusChanged.getContractId(),
+ contractStatusChanged.getContractLastModifiedOn()
+ );
+
+ try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
+ writer.write(objectMapper.writeValueAsString(event.getDetail()));
}
+ }
+
+ @Tracing
+ void saveContractStatus(String propertyId, String contractStatus, String contractId, Long contractLastModifiedOn) {
+ Map key = Map.of("property_id", AttributeValue.fromS(propertyId));
+
+ Map expressionAttributeValues = Map.of(
+ ":t", AttributeValue.fromS(contractStatus),
+ ":c", AttributeValue.fromS(contractId),
+ ":m", AttributeValue.fromN(String.valueOf(contractLastModifiedOn))
+ );
+
+ UpdateItemRequest updateItemRequest = UpdateItemRequest.builder()
+ .key(key)
+ .tableName(TABLE_NAME)
+ .updateExpression("set contract_status=:t, contract_last_modified_on=:m, contract_id=:c")
+ .expressionAttributeValues(expressionAttributeValues)
+ .build();
+
+ dynamodbClient.updateItem(updateItemRequest);
+ }
+
+ public void setDynamodbClient(DynamoDbClient dynamodbClient) {
+ this.dynamodbClient = dynamodbClient;
+ }
}
diff --git a/unicorn_approvals/ApprovalsService/src/main/java/approvals/PropertiesApprovalSyncFunction.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/PropertiesApprovalSyncFunction.java
index 5d70818..afcfb45 100644
--- a/unicorn_approvals/ApprovalsService/src/main/java/approvals/PropertiesApprovalSyncFunction.java
+++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/PropertiesApprovalSyncFunction.java
@@ -20,69 +20,41 @@
import java.io.Serializable;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
+/**
+ * Lambda function that processes DynamoDB stream events to sync property approval status
+ * with Step Functions workflows
+ */
public class PropertiesApprovalSyncFunction implements RequestHandler {
- Logger logger = LogManager.getLogger();
- SfnAsyncClient snfClient = SfnAsyncClient.builder()
- .httpClientBuilder(NettyNioAsyncHttpClient.builder()
- .maxConcurrency(100)
- .maxPendingConnectionAcquires(10_000))
- .build();
+ private static final Logger logger = LogManager.getLogger();
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+ private static final String APPROVED_STATUS = "APPROVED";
+
+ private final SfnAsyncClient sfnClient = SfnAsyncClient.builder()
+ .httpClientBuilder(NettyNioAsyncHttpClient.builder()
+ .maxConcurrency(100)
+ .maxPendingConnectionAcquires(10_000))
+ .build();
@Tracing
@Metrics(captureColdStart = true)
@Logging(logEvent = true)
public StreamsEventResponse handleRequest(DynamodbEvent input, Context context) {
-
List batchItemFailures = new ArrayList<>();
- String curRecordSequenceNumber = "";
for (DynamodbEvent.DynamodbStreamRecord dynamodbStreamRecord : input.getRecords()) {
+ String sequenceNumber = dynamodbStreamRecord.getDynamodb().getSequenceNumber();
+
try {
- // Process your record
-
- StreamRecord dynamodbRecord = dynamodbStreamRecord.getDynamodb();
- curRecordSequenceNumber = dynamodbRecord.getSequenceNumber();
- Map newImage = dynamodbRecord.getNewImage();
- Map oldImage = dynamodbRecord.getOldImage();
- if (oldImage == null) {
- oldImage = new HashMap();
- }
- if (newImage == null) {
- logger.debug("New image is null. Hence return empty stream response");
- return new StreamsEventResponse();
- }
- // if there is no token do nothing
- if (newImage.get("sfn_wait_approved_task_token") == null
- && oldImage.get("sfn_wait_approved_task_token") == null) {
- logger.debug("No task token in both the images. Hence return empty stream response");
- return new StreamsEventResponse();
- }
-
- // if contract status is approved, send the task token
-
- if (!newImage.get("contract_status").getS().equalsIgnoreCase("APPROVED")) {
- logger.debug("Contract status for property is not APPROVED : " +
- newImage.get("property_id").getS());
- return new StreamsEventResponse();
+ if (!processRecord(dynamodbStreamRecord)) {
+ continue; // Skip this record but don't fail
}
- logger.debug("Contract status for property is APPROVED : " +
- newImage.get("property_id").getS());
-
- // send task successful token
- taskSuccessful(newImage.get("sfn_wait_approved_task_token").getS(), newImage);
-
} catch (Exception e) {
- /*
- * Since we are working with streams, we can return the failed item immediately.
- * Lambda will immediately begin to retry processing from this failed item
- * onwards.
- */
- batchItemFailures.add(new StreamsEventResponse.BatchItemFailure(curRecordSequenceNumber));
+ logger.error("Failed to process record with sequence number: {}", sequenceNumber, e);
+ batchItemFailures.add(new StreamsEventResponse.BatchItemFailure(sequenceNumber));
return new StreamsEventResponse(batchItemFailures);
}
}
@@ -90,21 +62,55 @@ public StreamsEventResponse handleRequest(DynamodbEvent input, Context context)
return new StreamsEventResponse();
}
- private void taskSuccessful(String s, Map item) throws JsonProcessingException {
- // create the json structure and send the token
- ObjectMapper mapper = new ObjectMapper();
- ContractStatus contractStatus = new ContractStatus();
- contractStatus.setContract_id(item.get("contract_id").getS());
- contractStatus.setContract_status(item.get("contract_status").getS());
- contractStatus.setProperty_id(item.get("property_id").getS());
- contractStatus.setSfn_wait_approved_task_token(item.get("sfn_wait_approved_task_token").getS());
- String taskResult = mapper.writeValueAsString(contractStatus);
+ private boolean processRecord(DynamodbEvent.DynamodbStreamRecord streamRecord) throws JsonProcessingException {
+ StreamRecord dynamodbRecord = streamRecord.getDynamodb();
+ Map newImage = dynamodbRecord.getNewImage();
+ Map oldImage = dynamodbRecord.getOldImage();
- SendTaskSuccessRequest request = SendTaskSuccessRequest.builder()
- .taskToken(contractStatus.getSfn_wait_approved_task_token())
- .output(taskResult)
- .build();
- snfClient.sendTaskSuccess(request).join();
+ if (newImage == null) {
+ logger.debug("New image is null, skipping record");
+ return false;
+ }
+
+ if (!hasTaskToken(newImage, oldImage)) {
+ logger.debug("No task token found in either image, skipping record");
+ return false;
+ }
+
+ String contractStatus = newImage.get("contract_status").getS();
+ String propertyId = newImage.get("property_id").getS();
+
+ if (!APPROVED_STATUS.equalsIgnoreCase(contractStatus)) {
+ logger.debug("Contract status for property {} is not APPROVED: {}", propertyId, contractStatus);
+ return false;
+ }
+
+ logger.info("Contract approved for property: {}", propertyId);
+ sendTaskSuccess(newImage.get("sfn_wait_approved_task_token").getS(), newImage);
+ return true;
+ }
+
+ private boolean hasTaskToken(Map newImage, Map oldImage) {
+ return (newImage.get("sfn_wait_approved_task_token") != null) ||
+ (oldImage != null && oldImage.get("sfn_wait_approved_task_token") != null);
+ }
+
+ private void sendTaskSuccess(String taskToken, Map item) throws JsonProcessingException {
+ ContractStatus contractStatus = ContractStatus.builder()
+ .contractId(item.get("contract_id").getS())
+ .contractStatus(item.get("contract_status").getS())
+ .propertyId(item.get("property_id").getS())
+ .sfnWaitApprovedTaskToken(item.get("sfn_wait_approved_task_token").getS())
+ .build();
+ String taskResult = objectMapper.writeValueAsString(contractStatus);
+
+ SendTaskSuccessRequest request = SendTaskSuccessRequest.builder()
+ .taskToken(taskToken)
+ .output(taskResult)
+ .build();
+
+ sfnClient.sendTaskSuccess(request).join();
+ logger.info("Task success sent for property: {}", contractStatus.getPropertyId());
}
}
\ No newline at end of file
diff --git a/unicorn_approvals/ApprovalsService/src/main/java/approvals/WaitForContractApprovalFunction.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/WaitForContractApprovalFunction.java
index 7d0202e..c1f5f3e 100644
--- a/unicorn_approvals/ApprovalsService/src/main/java/approvals/WaitForContractApprovalFunction.java
+++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/WaitForContractApprovalFunction.java
@@ -19,89 +19,77 @@
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.CompletionException;
/**
- * Lambda handler to update the contract status change
+ * Lambda handler to wait for contract approval in Step Functions workflow
*/
public class WaitForContractApprovalFunction {
- Logger logger = LogManager.getLogger();
-
- final String TABLE_NAME = System.getenv("CONTRACT_STATUS_TABLE");
-
- DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder()
- .httpClientBuilder(NettyNioAsyncHttpClient.builder()
- .maxConcurrency(100)
- .maxPendingConnectionAcquires(10_000))
- .build();
-
- @Tracing
- @Metrics(captureColdStart = true)
- @Logging(logEvent = true)
- public void handleRequest(InputStream inputStream, OutputStream outputStream,
- Context context) throws IOException, ContractStatusNotFoundException {
-
- // deseralised to contract status
- ObjectMapper objectMapper = new ObjectMapper();
- String srtInput = new String(inputStream.readAllBytes());
- JsonNode event = objectMapper.readTree(srtInput);
- String propertyId = event.get("Input").get("property_id").asText();
- String taskToken = event.get("TaskToken").asText();
-
- logger.info("task Token : ", taskToken);
- logger.info("Property Id : ", propertyId);
-
- // get contract status
- Map dynamodbItem = getContractStatus(propertyId);
- updateTokenAndPauseExecution(taskToken, dynamodbItem.get("property_id").s());
-
- String responseString = event.get("Input").asText();
- OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
- logger.debug(responseString);
- writer.write(responseString);
- writer.close();
-
+ private static final Logger logger = LogManager.getLogger();
+ private static final String TABLE_NAME = System.getenv("CONTRACT_STATUS_TABLE");
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ private final DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder()
+ .httpClientBuilder(NettyNioAsyncHttpClient.builder()
+ .maxConcurrency(100)
+ .maxPendingConnectionAcquires(10_000))
+ .build();
+
+ @Tracing
+ @Metrics(captureColdStart = true)
+ @Logging(logEvent = true)
+ public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
+ throws IOException, ContractStatusNotFoundException {
+
+ String input = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+ JsonNode event = objectMapper.readTree(input);
+
+ String propertyId = event.get("Input").get("property_id").asText();
+ String taskToken = event.get("TaskToken").asText();
+
+ logger.info("Processing property: {} with task token: {}", propertyId, taskToken);
+
+ Map contractItem = getContractStatus(propertyId);
+ updateTokenAndPauseExecution(taskToken, contractItem.get("property_id").s());
+
+ String responseString = event.get("Input").toString();
+ try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
+ writer.write(responseString);
}
-
- private void updateTokenAndPauseExecution(String taskToken, String propertyId) {
- Map key = new HashMap();
- AttributeValue keyvalue = AttributeValue.fromS(propertyId);
- key.put("property_id", keyvalue);
-
- Map expressionAttributeValues = new HashMap();
- expressionAttributeValues.put(":g", AttributeValue.fromS(taskToken));
-
- UpdateItemRequest updateItemRequest = UpdateItemRequest.builder()
- .key(key)
- .tableName(TABLE_NAME)
- .updateExpression(
- "set sfn_wait_approved_task_token = :g")
- .expressionAttributeValues(expressionAttributeValues)
- .build();
- dynamodbClient.updateItem(updateItemRequest).join();
- }
-
- private Map getContractStatus(String propertyId)
- throws ContractStatusNotFoundException {
- HashMap keyToGet = new HashMap();
-
- keyToGet.put("property_id", AttributeValue.builder()
- .s(propertyId).build());
-
- GetItemRequest request = GetItemRequest.builder()
- .key(keyToGet)
- .tableName(TABLE_NAME)
- .build();
- Map returnvalue = null;
- try {
- returnvalue = dynamodbClient.getItem(request).join().item();
- } catch (Exception exception) {
- throw new ContractStatusNotFoundException(exception.getLocalizedMessage());
- }
-
- return returnvalue;
+ }
+
+ private void updateTokenAndPauseExecution(String taskToken, String propertyId) {
+ Map key = Map.of("property_id", AttributeValue.fromS(propertyId));
+ Map expressionAttributeValues = Map.of(":g", AttributeValue.fromS(taskToken));
+
+ UpdateItemRequest updateItemRequest = UpdateItemRequest.builder()
+ .key(key)
+ .tableName(TABLE_NAME)
+ .updateExpression("set sfn_wait_approved_task_token = :g")
+ .expressionAttributeValues(expressionAttributeValues)
+ .build();
+
+ dynamodbClient.updateItem(updateItemRequest).join();
+ }
+
+ private Map getContractStatus(String propertyId) throws ContractStatusNotFoundException {
+ Map key = Map.of("property_id", AttributeValue.fromS(propertyId));
+
+ GetItemRequest request = GetItemRequest.builder()
+ .key(key)
+ .tableName(TABLE_NAME)
+ .build();
+
+ try {
+ Map item = dynamodbClient.getItem(request).join().item();
+ if (item == null || item.isEmpty()) {
+ throw new ContractStatusNotFoundException("Contract status not found for property: " + propertyId);
+ }
+ return item;
+ } catch (CompletionException e) {
+ throw new ContractStatusNotFoundException("Failed to retrieve contract status: " + e.getCause().getMessage());
}
-
+ }
}
diff --git a/unicorn_approvals/ApprovalsService/src/main/java/approvals/dao/ContractStatus.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/dao/ContractStatus.java
index df99fcd..7244edc 100644
--- a/unicorn_approvals/ApprovalsService/src/main/java/approvals/dao/ContractStatus.java
+++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/dao/ContractStatus.java
@@ -1,47 +1,89 @@
package approvals.dao;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Data class representing contract status information
+ */
public class ContractStatus {
- String contract_id;
- String contract_status;
- String property_id;
- String sfn_wait_approved_task_token;
+
+ @JsonProperty("contract_id")
+ private final String contractId;
+
+ @JsonProperty("contract_status")
+ private final String contractStatus;
+
+ @JsonProperty("property_id")
+ private final String propertyId;
+
+ @JsonProperty("sfn_wait_approved_task_token")
+ private final String sfnWaitApprovedTaskToken;
- @Override
- public String toString() {
- return "Property [contract_id=" + contract_id + ", contract_status=" + contract_status + ", property_id="
- + property_id + ", sfn_wait_approved_task_token=" + sfn_wait_approved_task_token + "]";
+ private ContractStatus(Builder builder) {
+ this.contractId = builder.contractId;
+ this.contractStatus = builder.contractStatus;
+ this.propertyId = builder.propertyId;
+ this.sfnWaitApprovedTaskToken = builder.sfnWaitApprovedTaskToken;
}
- public String getContract_id() {
- return contract_id;
+ public String getContractId() {
+ return contractId;
}
- public void setContract_id(String contract_id) {
- this.contract_id = contract_id;
+ public String getContractStatus() {
+ return contractStatus;
}
- public String getContract_status() {
- return contract_status;
+ public String getPropertyId() {
+ return propertyId;
}
- public void setContract_status(String contract_status) {
- this.contract_status = contract_status;
+ public String getSfnWaitApprovedTaskToken() {
+ return sfnWaitApprovedTaskToken;
}
- public String getProperty_id() {
- return property_id;
+ public static Builder builder() {
+ return new Builder();
}
- public void setProperty_id(String property_id) {
- this.property_id = property_id;
- }
+ public static class Builder {
+ private String contractId;
+ private String contractStatus;
+ private String propertyId;
+ private String sfnWaitApprovedTaskToken;
- public String getSfn_wait_approved_task_token() {
- return sfn_wait_approved_task_token;
- }
+ public Builder contractId(String contractId) {
+ this.contractId = contractId;
+ return this;
+ }
+
+ public Builder contractStatus(String contractStatus) {
+ this.contractStatus = contractStatus;
+ return this;
+ }
- public void setSfn_wait_approved_task_token(String sfn_wait_approved_task_token) {
- this.sfn_wait_approved_task_token = sfn_wait_approved_task_token;
+ public Builder propertyId(String propertyId) {
+ this.propertyId = propertyId;
+ return this;
+ }
+
+ public Builder sfnWaitApprovedTaskToken(String sfnWaitApprovedTaskToken) {
+ this.sfnWaitApprovedTaskToken = sfnWaitApprovedTaskToken;
+ return this;
+ }
+
+ public ContractStatus build() {
+ return new ContractStatus(this);
+ }
}
+ @Override
+ public String toString() {
+ return "ContractStatus{" +
+ "contractId='" + contractId + '\'' +
+ ", contractStatus='" + contractStatus + '\'' +
+ ", propertyId='" + propertyId + '\'' +
+ ", sfnWaitApprovedTaskToken='" + sfnWaitApprovedTaskToken + '\'' +
+ '}';
+ }
}
diff --git a/unicorn_approvals/ApprovalsService/src/test/java/approvals/ContractStatusTests.java b/unicorn_approvals/ApprovalsService/src/test/java/approvals/ContractStatusTests.java
index 63caac2..c8e01da 100644
--- a/unicorn_approvals/ApprovalsService/src/test/java/approvals/ContractStatusTests.java
+++ b/unicorn_approvals/ApprovalsService/src/test/java/approvals/ContractStatusTests.java
@@ -2,53 +2,50 @@
import com.amazonaws.services.lambda.runtime.Context;
import org.junit.Before;
-import org.junit.jupiter.api.Test;
+import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
-import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import java.io.*;
-import java.util.HashMap;
-import java.util.Map;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
@RunWith(MockitoJUnitRunner.class)
public class ContractStatusTests {
- Context context;
- DynamoDbClient client;
-
- ContractStatusChangedHandlerFunction contractStatusChangedHandler;
-
- Map response = new HashMap();
-
- @Before
- public void setUp() {
-
- context = mock(Context.class);
- client = mock(DynamoDbClient.class);
-
- }
-
- @Test
- public void validStatusCheckEvent() throws IOException {
-
- contractStatusChangedHandler = new ContractStatusChangedHandlerFunction();
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- File resourceFile = new File("src/test/events/lambda/contract_status_changed.json");
- client = mock(DynamoDbClient.class);
- contractStatusChangedHandler.setDynamodbClient(client);
-
- FileInputStream fis = new FileInputStream(resourceFile);
-
- contractStatusChangedHandler.handleRequest(fis, outputStream, context);
- ByteArrayInputStream inStream = new ByteArrayInputStream(outputStream.toByteArray());
- String response = new String(inStream.readAllBytes());
- assertTrue("Successful Response", response.contains("contract_id"));
-
- }
-
+ @Mock
+ private Context context;
+
+ @Mock
+ private DynamoDbClient dynamoDbClient;
+
+ private ContractStatusChangedHandlerFunction contractStatusChangedHandler;
+
+ @Before
+ public void setUp() {
+ contractStatusChangedHandler = new ContractStatusChangedHandlerFunction();
+ contractStatusChangedHandler.setDynamodbClient(dynamoDbClient);
+ }
+
+ @Test
+ public void shouldProcessValidContractStatusChangeEvent() throws IOException {
+ // Given
+ Path testEventPath = Paths.get("src/test/events/lambda/contract_status_changed.json");
+
+ // When
+ try (InputStream inputStream = Files.newInputStream(testEventPath);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+
+ contractStatusChangedHandler.handleRequest(inputStream, outputStream, context);
+
+ // Then
+ String response = outputStream.toString();
+ assertTrue("Response should contain contract_id", response.contains("contract_id"));
+ }
+ }
}
diff --git a/unicorn_contracts/ContractsService/pom.xml b/unicorn_contracts/ContractsService/pom.xml
index f9b730e..ebd87fd 100644
--- a/unicorn_contracts/ContractsService/pom.xml
+++ b/unicorn_contracts/ContractsService/pom.xml
@@ -9,12 +9,12 @@
17
17
- 2.27.21
- 1.18.0
- 3.13.0
- 5.13.0
+ 2.32.29
+ 1.20.2
+ 3.16.1
+ 5.18.0
4.13.2
- 1.1.1
+ 1.1.2
@@ -37,36 +37,36 @@
com.amazonaws
aws-lambda-java-core
- 1.2.2
+ 1.3.0
com.fasterxml.jackson.core
jackson-databind
- 2.15.2
+ 2.18.4
com.fasterxml.jackson.core
jackson-core
- 2.15.2
+ 2.18.4
com.fasterxml.jackson.core
jackson-annotations
- 2.15.2
+ 2.18.4
org.apache.logging.log4j
log4j-api
- 2.20.0
+ 2.25.1
org.apache.logging.log4j
log4j-core
- 2.20.0
+ 2.25.1
@@ -123,17 +123,25 @@
org.apache.maven.plugins
maven-surefire-plugin
- 2.22.2
+ 3.5.3
+ false
handler
+
+
+ org.apache.maven.surefire
+ surefire-junit4
+ 3.5.3
+
+
org.apache.maven.plugins
maven-shade-plugin
- 3.2.4
+ 3.6.0
@@ -146,13 +154,13 @@
- dev.aspectj
+ org.codehaus.mojo
aspectj-maven-plugin
- 1.13.1
+ 1.15.0
+ 17
17
17
- 17
software.amazon.lambda
@@ -168,6 +176,13 @@
+
+
+ org.aspectj
+ aspectjtools
+ 1.9.22.1
+
+
@@ -179,9 +194,10 @@
org.apache.maven.plugins
maven-compiler-plugin
+ 3.14.0
- 16
- 16
+ 17
+ 17
diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java b/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java
index e6c3722..c1f2918 100644
--- a/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java
+++ b/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java
@@ -12,139 +12,186 @@
import org.apache.logging.log4j.Logger;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
-import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
-import software.amazon.lambda.powertools.metrics.MetricsUtils;
import software.amazon.lambda.powertools.tracing.Tracing;
-import java.util.Date;
-import java.util.HashMap;
+import java.time.Instant;
import java.util.Map;
+import java.util.Optional;
import java.util.UUID;
public class ContractEventHandler implements RequestHandler {
- // Initialise environment variables
- private static String DDB_TABLE = System.getenv("DYNAMODB_TABLE");
- ObjectMapper objectMapper = new ObjectMapper();
+ private static final String DDB_TABLE = System.getenv("DYNAMODB_TABLE");
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private static final Logger LOGGER = LogManager.getLogger(ContractEventHandler.class);
+ private static final String HTTP_METHOD_ATTR = "HttpMethod";
- DynamoDbClient dynamodbClient = DynamoDbClient.builder()
- .build();
+ private final DynamoDbClient dynamodbClient;
- Logger logger = LogManager.getLogger();
- MetricsLogger metricsLogger = MetricsUtils.metricsLogger();
+ public ContractEventHandler() {
+ this(DynamoDbClient.builder().build());
+ }
+
+ public ContractEventHandler(DynamoDbClient dynamodbClient) {
+ this.dynamodbClient = dynamodbClient;
+ }
@Override
+ @Tracing
public Void handleRequest(SQSEvent event, Context context) {
+ if (event == null || event.getRecords() == null) {
+ LOGGER.warn("Received null or empty SQS event");
+ return null;
+ }
for (SQSMessage msg : event.getRecords()) {
- // cehck in message attributes about the http method (HttpMethod)
- logger.debug(msg.toString());
- String httpMethod = msg.getMessageAttributes().get("HttpMethod").getStringValue();
- if ("POST".equalsIgnoreCase(httpMethod)) {
- try {
- createContract(msg.getBody());
- logger.debug("Contract Saved");
- } catch (JsonProcessingException jsonException) {
- logger.error("Unknown Exception occoured: " + jsonException.getMessage());
- logger.fatal(jsonException);
- jsonException.printStackTrace();
- }
-
- } else if ("PUT".equalsIgnoreCase(httpMethod)) {
- try {
- // update the event
- updateContract(msg.getBody());
- } catch (JsonProcessingException jsonException) {
- logger.error("Unknown Exception occoured: " + jsonException.getMessage());
- logger.fatal(jsonException);
- jsonException.printStackTrace();
- }
+ processMessage(msg);
+ }
+ return null;
+ }
+ private void processMessage(SQSMessage msg) {
+ LOGGER.debug("Processing message: {}", msg.getMessageId());
+
+ try {
+ String httpMethod = extractHttpMethod(msg);
+ String body = msg.getBody();
+
+ if (body == null || body.trim().isEmpty()) {
+ LOGGER.warn("Empty message body for message: {}", msg.getMessageId());
+ return;
}
+ switch (httpMethod.toUpperCase()) {
+ case "POST":
+ createContract(body);
+ LOGGER.info("Contract created successfully for message: {}", msg.getMessageId());
+ break;
+ case "PUT":
+ updateContract(body);
+ LOGGER.info("Contract updated successfully for message: {}", msg.getMessageId());
+ break;
+ default:
+ LOGGER.warn("Unsupported HTTP method: {} for message: {}", httpMethod, msg.getMessageId());
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error processing message {}: {}", msg.getMessageId(), e.getMessage(), e);
+ throw new RuntimeException("Failed to process contract message", e);
}
- return null;
+ }
+
+ private String extractHttpMethod(SQSMessage msg) {
+ return Optional.ofNullable(msg.getMessageAttributes())
+ .map(attrs -> attrs.get(HTTP_METHOD_ATTR))
+ .map(attr -> attr.getStringValue())
+ .orElseThrow(() -> new IllegalArgumentException("Missing HttpMethod attribute"));
}
@Tracing
- private void createContract(String strContract) throws JsonProcessingException {
+ private void createContract(String contractJson) throws JsonProcessingException {
+ Contract contract = OBJECT_MAPPER.readValue(contractJson, Contract.class);
+ validateContract(contract);
+
String contractId = UUID.randomUUID().toString();
- Long createDate = new Date().getTime();
- Contract contract = objectMapper.readValue(strContract, Contract.class);
-
- Map expressionValues = new HashMap<>();
- expressionValues.put(":cancelled", AttributeValue.builder().s(ContractStatusEnum.CANCELLED.name()).build());
- expressionValues.put(":closed", AttributeValue.builder().s(ContractStatusEnum.CLOSED.name()).build());
- expressionValues.put(":expired", AttributeValue.builder().s(ContractStatusEnum.EXPIRED.name()).build());
-
- HashMap itemValues = new HashMap<>();
- itemValues.put("property_id", AttributeValue.builder().s(contract.getPropertyId()).build());
- itemValues.put("seller_name", AttributeValue.builder().s(contract.getSellerName()).build());
- itemValues.put("contract_created",
- AttributeValue.builder().n(createDate.toString()).build());
- itemValues.put("contract_last_modified_on",
- AttributeValue.builder().n(createDate.toString()).build());
- itemValues.put("contract_id", AttributeValue.builder().s(contractId).build());
- itemValues.put("contract_status", AttributeValue.builder().s(ContractStatusEnum.DRAFT.name()).build());
-
- HashMap address = new HashMap<>();
- address.put("country", AttributeValue.builder().s(contract.getAddress().getCountry()).build());
- address.put("city", AttributeValue.builder().s(contract.getAddress().getCity()).build());
- address.put("street", AttributeValue.builder().s(contract.getAddress().getStreet()).build());
- address.put("number",
- AttributeValue.builder().n(Integer.valueOf(contract.getAddress().getNumber()).toString()).build());
-
- itemValues.put("address",
- AttributeValue.builder().m(address).build());
- PutItemRequest putItemRequest = PutItemRequest.builder().tableName(DDB_TABLE)
- .item(itemValues)
- .conditionExpression(
- "attribute_not_exists(property_id) OR contract_status IN (:cancelled , :closed, :expired)")
+ long timestamp = Instant.now().toEpochMilli();
+
+ Map item = Map.of(
+ "property_id", AttributeValue.builder().s(contract.getPropertyId()).build(),
+ "seller_name", AttributeValue.builder().s(contract.getSellerName()).build(),
+ "contract_created", AttributeValue.builder().n(String.valueOf(timestamp)).build(),
+ "contract_last_modified_on", AttributeValue.builder().n(String.valueOf(timestamp)).build(),
+ "contract_id", AttributeValue.builder().s(contractId).build(),
+ "contract_status", AttributeValue.builder().s(ContractStatusEnum.DRAFT.name()).build(),
+ "address", AttributeValue.builder().m(buildAddressMap(contract.getAddress())).build()
+ );
+
+ Map expressionValues = Map.of(
+ ":cancelled", AttributeValue.builder().s(ContractStatusEnum.CANCELLED.name()).build(),
+ ":closed", AttributeValue.builder().s(ContractStatusEnum.CLOSED.name()).build(),
+ ":expired", AttributeValue.builder().s(ContractStatusEnum.EXPIRED.name()).build()
+ );
+
+ PutItemRequest request = PutItemRequest.builder()
+ .tableName(DDB_TABLE)
+ .item(item)
+ .conditionExpression("attribute_not_exists(property_id) OR contract_status IN (:cancelled, :closed, :expired)")
.expressionAttributeValues(expressionValues)
.build();
+
try {
- dynamodbClient.putItem(putItemRequest);
- } catch (ConditionalCheckFailedException conditionalCheckFailedException) {
- logger.error("Unable to create contract for Property '" + contract.getPropertyId()
- + "'.There already is a contract for this property in status " + ContractStatusEnum.DRAFT + " or "
- + ContractStatusEnum.APPROVED);
+ dynamodbClient.putItem(request);
+ } catch (ConditionalCheckFailedException e) {
+ LOGGER.error("Active contract already exists for property: {}", contract.getPropertyId());
+ throw new IllegalStateException("Contract already exists for property: " + contract.getPropertyId(), e);
}
}
@Tracing
- private void updateContract(String strContract) throws JsonProcessingException {
- Contract contract = objectMapper.readValue(strContract, Contract.class);
- logger.info("Property ID is : " + contract.getPropertyId());
- HashMap itemKey = new HashMap<>();
-
- itemKey.put("property_id", AttributeValue.builder().s(contract.getPropertyId()).build());
-
- Map expressionAttributeValues = new HashMap<>();
- expressionAttributeValues.put(":draft", AttributeValue.builder().s(ContractStatusEnum.DRAFT.name()).build());
- expressionAttributeValues.put(":t", AttributeValue.builder().s(ContractStatusEnum.APPROVED.name()).build());
- expressionAttributeValues.put(":m", AttributeValue.builder().s(String.valueOf(new Date().getTime())).build());
+ private void updateContract(String contractJson) throws JsonProcessingException {
+ Contract contract = OBJECT_MAPPER.readValue(contractJson, Contract.class);
+ validateContractForUpdate(contract);
+
+ LOGGER.info("Updating contract for Property ID: {}", contract.getPropertyId());
+
+ Map key = Map.of(
+ "property_id", AttributeValue.builder().s(contract.getPropertyId()).build()
+ );
+
+ Map expressionValues = Map.of(
+ ":draft", AttributeValue.builder().s(ContractStatusEnum.DRAFT.name()).build(),
+ ":approved", AttributeValue.builder().s(ContractStatusEnum.APPROVED.name()).build(),
+ ":modifiedDate", AttributeValue.builder().n(String.valueOf(Instant.now().toEpochMilli())).build()
+ );
UpdateItemRequest request = UpdateItemRequest.builder()
.tableName(DDB_TABLE)
- .key(itemKey)
- .updateExpression("set contract_status=:t, modified_date=:m")
- .expressionAttributeValues(expressionAttributeValues)
- .conditionExpression(
- "attribute_exists(property_id) AND contract_status IN (:draft)")
+ .key(key)
+ .updateExpression("SET contract_status = :approved, contract_last_modified_on = :modifiedDate")
+ .expressionAttributeValues(expressionValues)
+ .conditionExpression("attribute_exists(property_id) AND contract_status = :draft")
.build();
+
try {
dynamodbClient.updateItem(request);
- } catch (ConditionalCheckFailedException conditionalCheckFailedException) {
- logger.error("Unable to update contract for Property '" + contract.getPropertyId()
- + "'.Status is not in " + ContractStatusEnum.DRAFT);
- } catch (ResourceNotFoundException conditionalCheckFailedException) {
- logger.error("Unable to update contract for Property '" + contract.getPropertyId()
- + "'. Not Found");
+ } catch (ConditionalCheckFailedException e) {
+ LOGGER.error("Contract not in DRAFT status for property: {}", contract.getPropertyId());
+ throw new IllegalStateException("Contract not in valid state for update: " + contract.getPropertyId(), e);
+ } catch (ResourceNotFoundException e) {
+ LOGGER.error("Contract not found for property: {}", contract.getPropertyId());
+ throw new IllegalArgumentException("Contract not found: " + contract.getPropertyId(), e);
}
}
- public void setDynamodbClient(DynamoDbClient dynamodbClient) {
- this.dynamodbClient = dynamodbClient;
+ private void validateContract(Contract contract) {
+ if (contract == null) {
+ throw new IllegalArgumentException("Contract cannot be null");
+ }
+ if (contract.getPropertyId() == null || contract.getPropertyId().trim().isEmpty()) {
+ throw new IllegalArgumentException("Property ID is required");
+ }
+ if (contract.getSellerName() == null || contract.getSellerName().trim().isEmpty()) {
+ throw new IllegalArgumentException("Seller name is required");
+ }
+ if (contract.getAddress() == null) {
+ throw new IllegalArgumentException("Address is required");
+ }
}
+ private void validateContractForUpdate(Contract contract) {
+ if (contract == null) {
+ throw new IllegalArgumentException("Contract cannot be null");
+ }
+ if (contract.getPropertyId() == null || contract.getPropertyId().trim().isEmpty()) {
+ throw new IllegalArgumentException("Property ID is required for update");
+ }
+ }
+
+ private Map buildAddressMap(contracts.utils.Address address) {
+ return Map.of(
+ "country", AttributeValue.builder().s(address.getCountry()).build(),
+ "city", AttributeValue.builder().s(address.getCity()).build(),
+ "street", AttributeValue.builder().s(address.getStreet()).build(),
+ "number", AttributeValue.builder().n(String.valueOf(address.getNumber())).build()
+ );
+ }
}
diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Address.java b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Address.java
index ec1be8d..d44a398 100644
--- a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Address.java
+++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Address.java
@@ -1,14 +1,34 @@
package contracts.utils;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Objects;
+
public class Address {
- String country;
- String city;
- String street;
- int number;
+ @JsonProperty("country")
+ private String country;
+
+ @JsonProperty("city")
+ private String city;
+
+ @JsonProperty("street")
+ private String street;
+
+ @JsonProperty("number")
+ private int number;
+
+ public Address() {}
+
+ public Address(String country, String city, String street, int number) {
+ this.country = country;
+ this.city = city;
+ this.street = street;
+ this.number = number;
+ }
public String getCountry() {
- return this.country;
+ return country;
}
public void setCountry(String country) {
@@ -16,7 +36,7 @@ public void setCountry(String country) {
}
public String getCity() {
- return this.city;
+ return city;
}
public void setCity(String city) {
@@ -24,7 +44,7 @@ public void setCity(String city) {
}
public String getStreet() {
- return this.street;
+ return street;
}
public void setStreet(String street) {
@@ -32,11 +52,36 @@ public void setStreet(String street) {
}
public int getNumber() {
- return this.number;
+ return number;
}
public void setNumber(int number) {
this.number = number;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Address address = (Address) o;
+ return number == address.number &&
+ Objects.equals(country, address.country) &&
+ Objects.equals(city, address.city) &&
+ Objects.equals(street, address.street);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(country, city, street, number);
+ }
+
+ @Override
+ public String toString() {
+ return "Address{" +
+ "country='" + country + '\'' +
+ ", city='" + city + '\'' +
+ ", street='" + street + '\'' +
+ ", number=" + number +
+ '}';
+ }
}
diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Contract.java b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Contract.java
index 067842f..14069c3 100644
--- a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Contract.java
+++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Contract.java
@@ -1,25 +1,43 @@
package contracts.utils;
import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Objects;
public class Contract {
- Address address;
+ @JsonProperty("address")
+ private Address address;
+
+ @JsonProperty("property_id")
@JsonAlias("property_id")
- String propertyId;
+ private String propertyId;
+
+ @JsonProperty("contract_id")
@JsonAlias("contract_id")
- String contractId;
+ private String contractId;
+
+ @JsonProperty("seller_name")
@JsonAlias("seller_name")
- String sellerName;
+ private String sellerName;
+
+ @JsonProperty("contract_status")
@JsonAlias("contract_status")
- ContractStatusEnum contractStatus;
+ private ContractStatusEnum contractStatus;
+
+ @JsonProperty("contract_created")
@JsonAlias("contract_created")
- Long contractCreated;
+ private Long contractCreated;
+
+ @JsonProperty("contract_last_modified_on")
@JsonAlias("contract_last_modified_on")
- Long contractLastModifiedOn;
+ private Long contractLastModifiedOn;
+
+ public Contract() {}
public Address getAddress() {
- return this.address;
+ return address;
}
public void setAddress(Address address) {
@@ -27,7 +45,7 @@ public void setAddress(Address address) {
}
public String getPropertyId() {
- return this.propertyId;
+ return propertyId;
}
public void setPropertyId(String propertyId) {
@@ -35,7 +53,7 @@ public void setPropertyId(String propertyId) {
}
public String getContractId() {
- return this.contractId;
+ return contractId;
}
public void setContractId(String contractId) {
@@ -43,7 +61,7 @@ public void setContractId(String contractId) {
}
public String getSellerName() {
- return this.sellerName;
+ return sellerName;
}
public void setSellerName(String sellerName) {
@@ -51,7 +69,7 @@ public void setSellerName(String sellerName) {
}
public ContractStatusEnum getContractStatus() {
- return this.contractStatus;
+ return contractStatus;
}
public void setContractStatus(ContractStatusEnum contractStatus) {
@@ -59,7 +77,7 @@ public void setContractStatus(ContractStatusEnum contractStatus) {
}
public Long getContractCreated() {
- return this.contractCreated;
+ return contractCreated;
}
public void setContractCreated(Long contractCreated) {
@@ -67,11 +85,34 @@ public void setContractCreated(Long contractCreated) {
}
public Long getContractLastModifiedOn() {
- return this.contractLastModifiedOn;
+ return contractLastModifiedOn;
}
public void setContractLastModifiedOn(Long contractLastModifiedOn) {
this.contractLastModifiedOn = contractLastModifiedOn;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Contract contract = (Contract) o;
+ return Objects.equals(propertyId, contract.propertyId) &&
+ Objects.equals(contractId, contract.contractId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(propertyId, contractId);
+ }
+
+ @Override
+ public String toString() {
+ return "Contract{" +
+ "propertyId='" + propertyId + '\'' +
+ ", contractId='" + contractId + '\'' +
+ ", sellerName='" + sellerName + '\'' +
+ ", contractStatus=" + contractStatus +
+ '}';
+ }
}
diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ContractStatusChangedEvent.java b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ContractStatusChangedEvent.java
index 362d0bd..0d0995a 100644
--- a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ContractStatusChangedEvent.java
+++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ContractStatusChangedEvent.java
@@ -2,15 +2,31 @@
import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Objects;
+
public class ContractStatusChangedEvent {
+
@JsonProperty("contract_last_modified_on")
- Long contractLastModifiedOn;
+ private Long contractLastModifiedOn;
+
@JsonProperty("contract_id")
- String contractId;
+ private String contractId;
+
@JsonProperty("property_id")
- String propertyId;
+ private String propertyId;
+
@JsonProperty("contract_status")
- ContractStatusEnum contractStatus;
+ private ContractStatusEnum contractStatus;
+
+ public ContractStatusChangedEvent() {}
+
+ public ContractStatusChangedEvent(String contractId, String propertyId,
+ ContractStatusEnum contractStatus, Long contractLastModifiedOn) {
+ this.contractId = contractId;
+ this.propertyId = propertyId;
+ this.contractStatus = contractStatus;
+ this.contractLastModifiedOn = contractLastModifiedOn;
+ }
public Long getContractLastModifiedOn() {
return contractLastModifiedOn;
@@ -44,4 +60,27 @@ public void setContractStatus(ContractStatusEnum contractStatus) {
this.contractStatus = contractStatus;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ContractStatusChangedEvent that = (ContractStatusChangedEvent) o;
+ return Objects.equals(contractId, that.contractId) &&
+ Objects.equals(propertyId, that.propertyId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(contractId, propertyId);
+ }
+
+ @Override
+ public String toString() {
+ return "ContractStatusChangedEvent{" +
+ "contractId='" + contractId + '\'' +
+ ", propertyId='" + propertyId + '\'' +
+ ", contractStatus=" + contractStatus +
+ ", contractLastModifiedOn=" + contractLastModifiedOn +
+ '}';
+ }
}
diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java
index 107963b..a17201a 100644
--- a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java
+++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java
@@ -1,29 +1,63 @@
package contracts.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import java.util.Map;
+import java.util.Optional;
public class ResponseParser {
- Contract parseResponse(Map queryResponse)
- throws JsonMappingException, JsonProcessingException {
- Contract response = new Contract();
- ObjectMapper objectMapper = new ObjectMapper();
- Address address = objectMapper.readValue(queryResponse.get("address").s(), Address.class);
- response.setAddress(address);
- response.setContractCreated(
- Long.valueOf(queryResponse.get("contract_created").s()));
- response.setContractId(queryResponse.get("contract_id").s());
- response.setContractLastModifiedOn(
- Long.valueOf(queryResponse.get("contract_last_modified_on").s()));
- response.setContractStatus(ContractStatusEnum.valueOf(queryResponse.get("contract_status").s()));
- response.setPropertyId(queryResponse.get("property_id").s());
- response.setSellerName(queryResponse.get("seller_name").s());
- return response;
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ public Contract parseResponse(Map queryResponse) throws JsonProcessingException {
+ if (queryResponse == null || queryResponse.isEmpty()) {
+ throw new IllegalArgumentException("Query response cannot be null or empty");
}
+
+ Contract contract = new Contract();
+
+ // Parse address
+ Optional.ofNullable(queryResponse.get("address"))
+ .map(AttributeValue::s)
+ .ifPresent(addressJson -> {
+ try {
+ Address address = OBJECT_MAPPER.readValue(addressJson, Address.class);
+ contract.setAddress(address);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to parse address", e);
+ }
+ });
+
+ // Parse other fields
+ Optional.ofNullable(queryResponse.get("contract_created"))
+ .map(AttributeValue::s)
+ .map(Long::valueOf)
+ .ifPresent(contract::setContractCreated);
+
+ Optional.ofNullable(queryResponse.get("contract_id"))
+ .map(AttributeValue::s)
+ .ifPresent(contract::setContractId);
+
+ Optional.ofNullable(queryResponse.get("contract_last_modified_on"))
+ .map(AttributeValue::s)
+ .map(Long::valueOf)
+ .ifPresent(contract::setContractLastModifiedOn);
+
+ Optional.ofNullable(queryResponse.get("contract_status"))
+ .map(AttributeValue::s)
+ .map(ContractStatusEnum::valueOf)
+ .ifPresent(contract::setContractStatus);
+
+ Optional.ofNullable(queryResponse.get("property_id"))
+ .map(AttributeValue::s)
+ .ifPresent(contract::setPropertyId);
+
+ Optional.ofNullable(queryResponse.get("seller_name"))
+ .map(AttributeValue::s)
+ .ifPresent(contract::setSellerName);
+
+ return contract;
+ }
}
diff --git a/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java b/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java
index b3b10af..aec7b53 100644
--- a/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java
+++ b/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java
@@ -2,39 +2,108 @@
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.SQSEvent;
-import com.amazonaws.services.lambda.runtime.tests.annotations.Event;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent.MessageAttribute;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
import org.junit.Before;
-import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.PutItemResponse;
-import static org.mockito.Mockito.mock;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class CreateContractTests {
- Context context;
+ @Mock
+ private Context context;
+
+ @Mock
+ private DynamoDbClient dynamoDbClient;
+
+ private ContractEventHandler handler;
+
+ @Before
+ public void setUp() {
+ handler = new ContractEventHandler(dynamoDbClient);
+ }
+
+ @Test
+ public void shouldProcessValidCreateEvent() {
+ // Given
+ SQSEvent event = createTestEvent("POST",
+ "{ \"address\": { \"country\": \"USA\", \"city\": \"Anytown\", \"street\": \"Main Street\", \"number\": 123 }, \"seller_name\": \"John Smith\", \"property_id\": \"usa/anytown/main-street/123\"}");
+
+ when(dynamoDbClient.putItem(any(PutItemRequest.class)))
+ .thenReturn(PutItemResponse.builder().build());
- ContractEventHandler handler;
+ // When
+ handler.handleRequest(event, context);
- DynamoDbClient client;
+ // Then
+ verify(dynamoDbClient, times(1)).putItem(any(PutItemRequest.class));
+ }
- @Before
- public void setUp() {
+ @Test
+ public void shouldHandleNullEvent() {
+ // When
+ handler.handleRequest(null, context);
+
+ // Then
+ verifyNoInteractions(dynamoDbClient);
+ }
- client = mock(DynamoDbClient.class);
- context = mock(Context.class);
+ @Test
+ public void shouldHandleEmptyEvent() {
+ // Given
+ SQSEvent emptyEvent = new SQSEvent();
+
+ // When
+ handler.handleRequest(emptyEvent, context);
+
+ // Then
+ verifyNoInteractions(dynamoDbClient);
+ }
- }
+ @Test(expected = RuntimeException.class)
+ public void shouldThrowExceptionForMissingHttpMethod() {
+ // Given
+ SQSEvent event = createTestEventWithoutHttpMethod(
+ "{ \"address\": { \"country\": \"USA\", \"city\": \"Anytown\", \"street\": \"Main Street\", \"number\": 123 }, \"seller_name\": \"John Smith\", \"property_id\": \"usa/anytown/main-street/123\"}");
+
+ // When
+ handler.handleRequest(event, context);
+ }
- @ParameterizedTest
- @Event(value = "src/test/events/create_valid_event.json", type = SQSEvent.class)
- public void validEvent(SQSEvent event) {
- DynamoDbClient client = mock(DynamoDbClient.class);
- handler = new ContractEventHandler();
- handler.setDynamodbClient(client);
- handler.handleRequest(event, context);
- }
+ private SQSEvent createTestEvent(String httpMethod, String body) {
+ SQSEvent event = new SQSEvent();
+ SQSMessage message = new SQSMessage();
+ message.setMessageId("test-message-id");
+ message.setBody(body);
+
+ MessageAttribute httpMethodAttr = new MessageAttribute();
+ httpMethodAttr.setStringValue(httpMethod);
+ message.setMessageAttributes(Map.of("HttpMethod", httpMethodAttr));
+
+ event.setRecords(Collections.singletonList(message));
+ return event;
+ }
+ private SQSEvent createTestEventWithoutHttpMethod(String body) {
+ SQSEvent event = new SQSEvent();
+ SQSMessage message = new SQSMessage();
+ message.setMessageId("test-message-id");
+ message.setBody(body);
+ message.setMessageAttributes(Collections.emptyMap());
+
+ event.setRecords(Collections.singletonList(message));
+ return event;
+ }
}
diff --git a/unicorn_web/Common/pom.xml b/unicorn_web/Common/pom.xml
index 4178d15..2d86d38 100644
--- a/unicorn_web/Common/pom.xml
+++ b/unicorn_web/Common/pom.xml
@@ -22,13 +22,13 @@
software.amazon.awssdk
dynamodb-enhanced
- 2.27.21
+ 2.32.29
compile
com.fasterxml.jackson.core
jackson-annotations
- 2.15.3
+ 2.18.4
compile
diff --git a/unicorn_web/Common/src/main/java/dao/Property.java b/unicorn_web/Common/src/main/java/dao/Property.java
index d4b53af..de0be86 100644
--- a/unicorn_web/Common/src/main/java/dao/Property.java
+++ b/unicorn_web/Common/src/main/java/dao/Property.java
@@ -1,6 +1,7 @@
package dao;
import java.util.List;
+import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -12,26 +13,29 @@
@DynamoDbBean
public class Property {
- String country;
- String city;
- String street;
- String propertyNumber;
- String description;
- String contract;
- Float listprice;
- String currency;
- List images;
- String status;
+ private String country;
+ private String city;
+ private String street;
+ private String propertyNumber;
+ private String description;
+ private String contract;
+ private Float listprice;
+ private String currency;
+ private List images;
+ private String status;
@JsonIgnore
- String pk;
+ private String pk;
@JsonIgnore
- String sk;
- String id;
+ private String sk;
+ private String id;
@DynamoDbPartitionKey
@DynamoDbAttribute("PK")
public String getPk() {
- return ("PROPERTY#" + getCountry() + "#" + getCity()).replace(' ', '-').toLowerCase();
+ if (country == null || city == null) {
+ return pk; // Return stored value if components are null
+ }
+ return ("PROPERTY#" + country + "#" + city).replace(' ', '-').toLowerCase();
}
public void setPk(String pk) {
@@ -41,7 +45,10 @@ public void setPk(String pk) {
@DynamoDbSortKey
@DynamoDbAttribute("SK")
public String getSk() {
- return (getStreet() + "#" + getPropertyNumber()).replace(' ', '-').toLowerCase();
+ if (street == null || propertyNumber == null) {
+ return sk; // Return stored value if components are null
+ }
+ return (street + "#" + propertyNumber).replace(' ', '-').toLowerCase();
}
public void setSk(String sk) {
@@ -51,7 +58,15 @@ public void setSk(String sk) {
@JsonIgnore
@software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore
public String getId() {
- return (getPk() + '/' + getSk()).replace('#', '/');
+ if (id != null) {
+ return id;
+ }
+ String partitionKey = getPk();
+ String sortKey = getSk();
+ if (partitionKey != null && sortKey != null) {
+ return (partitionKey + '/' + sortKey).replace('#', '/');
+ }
+ return null;
}
public void setId(String id) {
@@ -140,11 +155,28 @@ public void setStatus(String status) {
}
@Override
- public String toString() {
- return "Property [city=" + city + ", contract=" + contract + ", country=" + country + ", currency=" + currency
- + ", description=" + description + ", id=" + getId() + ", images=" + images + ", listprice=" + listprice
- + ", pk=" + getPk() + ", propertyNumber=" + propertyNumber + ", sk=" + getSk() + ", status=" + status
- + ", street=" + street + "]";
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Property property = (Property) o;
+ return Objects.equals(getId(), property.getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getId());
}
+ @Override
+ public String toString() {
+ return "Property{" +
+ "country='" + country + '\'' +
+ ", city='" + city + '\'' +
+ ", street='" + street + '\'' +
+ ", propertyNumber='" + propertyNumber + '\'' +
+ ", status='" + status + '\'' +
+ ", listprice=" + listprice +
+ ", currency='" + currency + '\'' +
+ '}';
+ }
}
diff --git a/unicorn_web/PublicationManagerService/pom.xml b/unicorn_web/PublicationManagerService/pom.xml
index 9ec828c..ace1280 100644
--- a/unicorn_web/PublicationManagerService/pom.xml
+++ b/unicorn_web/PublicationManagerService/pom.xml
@@ -9,15 +9,14 @@
17
17
- 2.27.21
- 1.18.0
- 3.13.0
- 5.13.0
+ 2.32.29
+ 1.20.2
+ 3.16.1
+ 5.18.0
4.13.2
- 1.1.1
- 1.2.3
- 3.13.0
- 2.27.21
+ 1.1.2
+ 1.3.0
+ 2.32.29
@@ -118,36 +117,36 @@
com.amazonaws
aws-lambda-java-core
- 1.2.2
+ ${aws-lambda-java-core.version}
com.fasterxml.jackson.core
jackson-databind
- 2.15.2
+ 2.18.4
com.fasterxml.jackson.core
jackson-core
- 2.15.2
+ 2.18.4
com.fasterxml.jackson.core
jackson-annotations
- 2.15.2
+ 2.18.4
org.apache.logging.log4j
log4j-api
- 2.20.0
+ 2.25.1
org.apache.logging.log4j
log4j-core
- 2.20.0
+ 2.25.1
common
@@ -164,7 +163,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 2.22.2
+ 3.5.3
handler
@@ -174,7 +173,7 @@
org.apache.maven.plugins
maven-shade-plugin
- 3.2.4
+ 3.6.0
@@ -187,12 +186,10 @@
- dev.aspectj
+ org.codehaus.mojo
aspectj-maven-plugin
- 1.13.1
+ 1.15.0
- 17
- 17
17
@@ -216,11 +213,18 @@
+
+
+ org.aspectj
+ aspectjtools
+ 1.9.22.1
+
+
org.apache.maven.plugins
maven-compiler-plugin
- 3.11.0
+ 3.14.0
17
17
diff --git a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java
index ef882de..1faef8b 100644
--- a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java
+++ b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java
@@ -5,6 +5,7 @@
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
+import java.util.Map;
import com.amazonaws.services.lambda.runtime.Context;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -27,74 +28,100 @@
import schema.unicorn_approvals.publicationevaluationcompleted.PublicationEvaluationCompleted;
/**
- * Function checks for the existence of a contract status entry for a specified
- * search.
- * If an entry exists, pause the workflow, and update the record with task
- * token.
+ * Processes publication evaluation completed events and updates property status.
*/
public class PublicationApprovedEventHandler {
- Logger logger = LogManager.getLogger();
+ private static final Logger logger = LogManager.getLogger(PublicationApprovedEventHandler.class);
+
+ private final String tableName = System.getenv("DYNAMODB_TABLE");
+ private final DynamoDbAsyncTable propertyTable;
+ private final ObjectMapper objectMapper = new ObjectMapper();
- final String TABLE_NAME = System.getenv("DYNAMODB_TABLE");
+ public PublicationApprovedEventHandler() {
DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder()
- .httpClientBuilder(NettyNioAsyncHttpClient.builder())
- .build();
+ .httpClientBuilder(NettyNioAsyncHttpClient.builder())
+ .build();
DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder()
- .dynamoDbClient(dynamodbClient)
- .build();
-
- DynamoDbAsyncTable propertyTable = enhancedClient.table(TABLE_NAME,
- TableSchema.fromBean(Property.class));
-
- @Tracing
- @Metrics(captureColdStart = true)
- @Logging(logEvent = true)
- public void handleRequest(InputStream inputStream, OutputStream outputStream,
- Context context) throws IOException {
-
- AWSEvent event = Marshaller.unmarshalEvent(inputStream,
- PublicationEvaluationCompleted.class);
-
- String propertyId = event.getDetail().getPropertyId();
- String evaluationResult = event.getDetail().getEvaluationResult();
-
- publicationApproved(evaluationResult, propertyId);
-
- ObjectMapper objectMapper = new ObjectMapper();
- OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
- writer.write(objectMapper.writeValueAsString("'result': 'Successfully updated search status'"));
- writer.close();
-
+ .dynamoDbClient(dynamodbClient)
+ .build();
+
+ this.propertyTable = enhancedClient.table(tableName, TableSchema.fromBean(Property.class));
+ }
+
+ @Tracing
+ @Metrics(captureColdStart = true)
+ @Logging(logEvent = true)
+ public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
+ try {
+ AWSEvent event = Marshaller.unmarshalEvent(inputStream,
+ PublicationEvaluationCompleted.class);
+
+ if (event.getDetail() == null) {
+ throw new IllegalArgumentException("Event detail is null");
+ }
+
+ String propertyId = event.getDetail().getPropertyId();
+ String evaluationResult = event.getDetail().getEvaluationResult();
+
+ if (propertyId == null || propertyId.trim().isEmpty()) {
+ throw new IllegalArgumentException("Property ID is null or empty");
+ }
+
+ if (evaluationResult == null || evaluationResult.trim().isEmpty()) {
+ throw new IllegalArgumentException("Evaluation result is null or empty");
+ }
+
+ updatePropertyStatus(evaluationResult, propertyId);
+
+ String response = objectMapper.writeValueAsString(
+ Map.of("result", "Successfully updated property status"));
+
+ try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
+ writer.write(response);
+ }
+
+ } catch (Exception e) {
+ logger.error("Error processing publication evaluation event", e);
+ String errorResponse = objectMapper.writeValueAsString(
+ Map.of("error", "Failed to process event: " + e.getMessage()));
+
+ try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
+ writer.write(errorResponse);
+ }
+ throw new RuntimeException("Event processing failed", e);
}
-
- @Tracing
- private void publicationApproved(String evaluationResult, String propertyId) {
-
- String[] splitString = propertyId.split("/");
- String country = splitString[0];
- String city = splitString[1];
- String street = splitString[2];
- String number = splitString[3];
- String strPartionKey = ("search#" + country + "#" + city).replace(' ', '-').toLowerCase();
- String strSortKey = (street + "#" + number).replace(' ', '-').toLowerCase();
-
- Key key = Key.builder().partitionValue(strPartionKey).sortValue(strSortKey).build();
- Property existingProperty = propertyTable.getItem(key).join();
-
- if (existingProperty == null) {
- logger.error("Property not found for ID: {}", propertyId);
- throw new RuntimeException("Property not found with ID: " + propertyId);
- }
-
- // Always set the search number explicitly to ensure it's correct
- existingProperty.setPropertyNumber(number);
- existingProperty.setStatus(evaluationResult);
-
- logger.info("Updating search with status: {} and propertyNumber: {}",
- evaluationResult, existingProperty.getPropertyNumber());
- propertyTable.putItem(existingProperty).join();
+ }
+
+ @Tracing
+ private void updatePropertyStatus(String evaluationResult, String propertyId) {
+ try {
+ String[] parts = propertyId.split("/");
+ if (parts.length != 4) {
+ throw new IllegalArgumentException("Invalid property ID format: " + propertyId);
+ }
+
+ String partitionKey = ("search#" + parts[0] + "#" + parts[1]).replace(' ', '-').toLowerCase();
+ String sortKey = (parts[2] + "#" + parts[3]).replace(' ', '-').toLowerCase();
+
+ Key key = Key.builder().partitionValue(partitionKey).sortValue(sortKey).build();
+ Property existingProperty = propertyTable.getItem(key).join();
+
+ if (existingProperty == null) {
+ logger.error("Property not found for ID: {}", propertyId);
+ throw new RuntimeException("Property not found with ID: " + propertyId);
+ }
+
+ existingProperty.setPropertyNumber(parts[3]);
+ existingProperty.setStatus(evaluationResult);
+
+ logger.info("Updating property {} with status: {}", propertyId, evaluationResult);
+ propertyTable.putItem(existingProperty).join();
+
+ } catch (Exception e) {
+ logger.error("Failed to update property status for ID: {}", propertyId, e);
+ throw new RuntimeException("Property update failed", e);
}
-
+ }
}
diff --git a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java
index ed954fe..47e944f 100644
--- a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java
+++ b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java
@@ -9,7 +9,6 @@
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.amazonaws.services.lambda.runtime.Context;
@@ -43,152 +42,168 @@
import software.amazon.lambda.powertools.tracing.Tracing;
/**
- * Validates the integrity of the search content
+ * Validates property requests and sends approval events.
*/
public class RequestApprovalFunction {
- Logger logger = LogManager.getLogger();
- Set noActionSet = new HashSet(Arrays.asList("APPROVED"));
- String SERVICE = "Unicorn.Web";
- String EXPRESSION = "[a-z-]+\\/[a-z-]+\\/[a-z][a-z0-9-]*\\/[0-9-]+";
- String TARGET_STATE = "PENDING";
- Pattern pattern = Pattern.compile(EXPRESSION);
-
- String TABLE_NAME = System.getenv("DYNAMODB_TABLE");
- String EVENT_BUS = System.getenv("EVENT_BUS");
-
- DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder()
- .httpClientBuilder(NettyNioAsyncHttpClient.builder())
- .build();
-
- DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder()
- .dynamoDbClient(dynamodbClient)
- .build();
-
- DynamoDbAsyncTable propertyTable = enhancedClient.table(TABLE_NAME,
- TableSchema.fromBean(Property.class));
-
- EventBridgeAsyncClient eventBridgeClient = EventBridgeAsyncClient.builder()
- .httpClientBuilder(NettyNioAsyncHttpClient.builder())
- .build();
-
- ObjectMapper objectMapper = new ObjectMapper();
+ private static final Logger logger = LogManager.getLogger(RequestApprovalFunction.class);
+ private static final Set NO_ACTION_STATUSES = new HashSet<>(Arrays.asList("APPROVED"));
+ private static final String PROPERTY_ID_PATTERN = "[a-z-]+\\/[a-z-]+\\/[a-z][a-z0-9-]*\\/[0-9-]+";
+ private static final String CONTENT_TYPE = "application/json";
+
+ private final Pattern propertyIdPattern = Pattern.compile(PROPERTY_ID_PATTERN);
+ private final String tableName = System.getenv("DYNAMODB_TABLE");
+ private final String eventBus = System.getenv("EVENT_BUS");
+
+ private final DynamoDbAsyncTable propertyTable;
+ private final EventBridgeAsyncClient eventBridgeClient;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ public RequestApprovalFunction() {
+ DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder()
+ .httpClientBuilder(NettyNioAsyncHttpClient.builder())
+ .build();
+
+ DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder()
+ .dynamoDbClient(dynamodbClient)
+ .build();
+
+ this.propertyTable = enhancedClient.table(tableName, TableSchema.fromBean(Property.class));
+ this.eventBridgeClient = EventBridgeAsyncClient.builder()
+ .httpClientBuilder(NettyNioAsyncHttpClient.builder())
+ .build();
+ }
@Tracing
@Metrics(captureColdStart = true)
@Logging(logEvent = true, correlationIdPath = CorrelationIdPathConstants.API_GATEWAY_REST)
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input,
- final Context context) throws JsonProcessingException {
- {
-
- Map headers = new HashMap<>();
- headers.put("Content-Type", "application/json");
- headers.put("X-Custom-Header", "application/json");
- APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
- .withHeaders(headers);
+ final Context context) {
+ try {
+ if (input.getBody() == null || input.getBody().trim().isEmpty()) {
+ return createErrorResponse(400, "Request body is required");
+ }
JsonNode rootNode = objectMapper.readTree(input.getBody());
- String propertyId = rootNode.get("property_id").asText();
- Matcher matcher = pattern.matcher(propertyId);
- boolean valid = matcher.matches();
- if (!valid) {
- return response
- .withBody("Input invalid; must conform to regular expression: " + EXPRESSION)
- .withStatusCode(500);
+ JsonNode propertyIdNode = rootNode.get("property_id");
+
+ if (propertyIdNode == null) {
+ return createErrorResponse(400, "property_id field is required");
}
- String[] splitString = propertyId.split("/");
- String country = splitString[0];
- String city = splitString[1];
- String street = splitString[2];
- String number = splitString[3];
- String strPartionKey = ("search#" + country + "#" + city).replace(' ', '-').toLowerCase();
- String strSortKey = (street + "#" + number).replace(' ', '-').toLowerCase();
- try {
- List properties = queryTable(strPartionKey, strSortKey);
- if (properties.size() <= 0) {
- return response
- .withBody("No search found in database with the requested search id")
- .withStatusCode(500);
- }
- Property property = properties.get(0);
- if (noActionSet.contains(property.getStatus())) {
- return response
- .withStatusCode(200)
- .withBody("'result': 'Property is already " + property.getStatus() + "; no action taken'");
- }
- sendEvent(property);
-
- } catch (Exception e) {
- return response
- .withBody("Error in searching")
- .withStatusCode(500);
+
+ String propertyId = propertyIdNode.asText();
+ if (!propertyIdPattern.matcher(propertyId).matches()) {
+ return createErrorResponse(400, "Invalid property_id format. Must match: " + PROPERTY_ID_PATTERN);
}
- return response
- .withStatusCode(200)
- .withBody("'result': 'Approval Requested'");
+ PropertyComponents components = parsePropertyId(propertyId);
+ List properties = queryTable(components.partitionKey, components.sortKey);
+
+ if (properties.isEmpty()) {
+ return createErrorResponse(404, "Property not found");
+ }
+
+ Property property = properties.get(0);
+ if (NO_ACTION_STATUSES.contains(property.getStatus())) {
+ return createSuccessResponse("Property is already " + property.getStatus() + "; no action taken");
+ }
+
+ sendEvent(property);
+ return createSuccessResponse("Approval requested successfully");
+
+ } catch (JsonProcessingException e) {
+ logger.error("Invalid JSON in request body", e);
+ return createErrorResponse(400, "Invalid JSON format");
+ } catch (Exception e) {
+ logger.error("Error processing approval request", e);
+ return createErrorResponse(500, "Internal server error");
}
+ }
+ private PropertyComponents parsePropertyId(String propertyId) {
+ String[] parts = propertyId.split("/");
+ String partitionKey = ("search#" + parts[0] + "#" + parts[1]).replace(' ', '-').toLowerCase();
+ String sortKey = (parts[2] + "#" + parts[3]).replace(' ', '-').toLowerCase();
+ return new PropertyComponents(partitionKey, sortKey);
}
- public List queryTable(String partitionkey, String sortKey) throws Exception {
+ private APIGatewayProxyResponseEvent createSuccessResponse(String message) {
+ String body = String.format("{\"result\":\"%s\"}", message);
+ return new APIGatewayProxyResponseEvent()
+ .withStatusCode(200)
+ .withHeaders(Map.of("Content-Type", CONTENT_TYPE))
+ .withBody(body);
+ }
- try {
- if (partitionkey == null || sortKey == null) {
- throw new Exception("Invalid Input");
- }
- List result = new ArrayList();
- SdkPublisher properties = null;
+ private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) {
+ String body = String.format("{\"error\":\"%s\"}", message);
+ return new APIGatewayProxyResponseEvent()
+ .withStatusCode(statusCode)
+ .withHeaders(Map.of("Content-Type", CONTENT_TYPE))
+ .withBody(body);
+ }
- Key key = Key.builder().partitionValue(partitionkey).sortValue(sortKey).build();
+ private static class PropertyComponents {
+ final String partitionKey;
+ final String sortKey;
- QueryConditional queryConditional = QueryConditional.sortBeginsWith(key);
- QueryEnhancedRequest request = QueryEnhancedRequest.builder().queryConditional(queryConditional)
- .build();
- properties = propertyTable.query(request).items();
+ PropertyComponents(String partitionKey, String sortKey) {
+ this.partitionKey = partitionKey;
+ this.sortKey = sortKey;
+ }
+ }
- CompletableFuture future = properties.subscribe(res -> {
- // Add response to the list
- result.add(res);
- });
- future.get();
+ private List queryTable(String partitionKey, String sortKey) throws Exception {
+ if (partitionKey == null || sortKey == null) {
+ throw new IllegalArgumentException("Partition key and sort key cannot be null");
+ }
- return result;
+ List result = new ArrayList<>();
+ Key key = Key.builder().partitionValue(partitionKey).sortValue(sortKey).build();
+ QueryConditional queryConditional = QueryConditional.sortBeginsWith(key);
+ QueryEnhancedRequest request = QueryEnhancedRequest.builder()
+ .queryConditional(queryConditional)
+ .build();
+ try {
+ SdkPublisher properties = propertyTable.query(request).items();
+ CompletableFuture future = properties.subscribe(result::add);
+ future.get();
+ return result;
} catch (DynamoDbException | InterruptedException | ExecutionException e) {
- throw new Exception(e.getMessage());
+ logger.error("Error querying DynamoDB", e);
+ throw new Exception("Database query failed: " + e.getMessage());
}
}
@Tracing
@Metrics
- public String sendEvent(Property property)
- throws JsonProcessingException {
-
+ private void sendEvent(Property property) throws JsonProcessingException {
RequestApproval event = new RequestApproval();
event.setPropertyId(property.getId());
+
Address address = new Address();
address.setCity(property.getCity());
address.setCountry(property.getCountry());
address.setNumber(property.getPropertyNumber());
event.setAddress(address);
- String event_string = objectMapper.writeValueAsString(event);
-
- List requestEntries = new ArrayList();
+ String eventString = objectMapper.writeValueAsString(event);
- requestEntries.add(PutEventsRequestEntry.builder()
- .eventBusName(EVENT_BUS)
+ PutEventsRequestEntry requestEntry = PutEventsRequestEntry.builder()
+ .eventBusName(eventBus)
.source("Unicorn.Web")
.resources(property.getId())
.detailType("PublicationApprovalRequested")
- .detail(event_string).build());
+ .detail(eventString)
+ .build();
- PutEventsRequest eventsRequest = PutEventsRequest.builder().entries(requestEntries).build();
+ PutEventsRequest eventsRequest = PutEventsRequest.builder()
+ .entries(requestEntry)
+ .build();
eventBridgeClient.putEvents(eventsRequest).join();
-
- return event_string;
+ logger.info("Event sent successfully for property: {}", property.getId());
}
}
@@ -212,11 +227,13 @@ public Address getAddress() {
public void setAddress(Address address) {
this.address = address;
}
-
}
class Address {
String country;
+ String city;
+ String state;
+ String number;
public String getCountry() {
return country;
@@ -249,9 +266,4 @@ public String getNumber() {
public void setNumber(String number) {
this.number = number;
}
-
- String city;
- String state;
- String number;
-
}
diff --git a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/Address.class b/unicorn_web/PublicationManagerService/target/classes/publicationmanager/Address.class
deleted file mode 100644
index a3589f5..0000000
Binary files a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/Address.class and /dev/null differ
diff --git a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/PublicationApprovedEventHandler.class b/unicorn_web/PublicationManagerService/target/classes/publicationmanager/PublicationApprovedEventHandler.class
deleted file mode 100644
index bab0b47..0000000
Binary files a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/PublicationApprovedEventHandler.class and /dev/null differ
diff --git a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApproval.class b/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApproval.class
deleted file mode 100644
index 5087c6d..0000000
Binary files a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApproval.class and /dev/null differ
diff --git a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApprovalFunction.class b/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApprovalFunction.class
deleted file mode 100644
index d8631ed..0000000
Binary files a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApprovalFunction.class and /dev/null differ
diff --git a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/AWSEvent.class b/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/AWSEvent.class
deleted file mode 100644
index 05fd90d..0000000
Binary files a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/AWSEvent.class and /dev/null differ
diff --git a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted.class b/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted.class
deleted file mode 100644
index 546342c..0000000
Binary files a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted.class and /dev/null differ
diff --git a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller.class b/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller.class
deleted file mode 100644
index 94d71b9..0000000
Binary files a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller.class and /dev/null differ
diff --git a/unicorn_web/SearchService/pom.xml b/unicorn_web/SearchService/pom.xml
index 2e7b01e..b627290 100644
--- a/unicorn_web/SearchService/pom.xml
+++ b/unicorn_web/SearchService/pom.xml
@@ -9,15 +9,14 @@
17
17
- 2.27.21
- 1.18.0
- 3.13.0
- 5.13.0
+ 2.32.29
+ 1.20.2
+ 3.16.1
+ 5.18.0
4.13.2
- 1.1.1
- 1.2.3
- 3.13.0
- 2.27.21
+ 1.1.2
+ 1.3.0
+ 2.32.29
@@ -124,36 +123,36 @@
com.amazonaws
aws-lambda-java-core
- 1.2.2
+ ${aws-lambda-java-core.version}
com.fasterxml.jackson.core
jackson-databind
- 2.15.2
+ 2.18.4
com.fasterxml.jackson.core
jackson-core
- 2.15.2
+ 2.18.4
com.fasterxml.jackson.core
jackson-annotations
- 2.15.2
+ 2.18.4
org.apache.logging.log4j
log4j-api
- 2.20.0
+ 2.25.1
org.apache.logging.log4j
log4j-core
- 2.20.0
+ 2.25.1
common
@@ -170,7 +169,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 2.22.2
+ 3.5.3
handler
@@ -180,7 +179,7 @@
org.apache.maven.plugins
maven-shade-plugin
- 3.2.4
+ 3.6.0
@@ -193,12 +192,10 @@
- dev.aspectj
+ org.codehaus.mojo
aspectj-maven-plugin
- 1.13.1
+ 1.15.0
- 17
- 17
17
@@ -222,11 +219,18 @@
+
+
+ org.aspectj
+ aspectjtools
+ 1.9.22.1
+
+
org.apache.maven.plugins
maven-compiler-plugin
- 3.11.0
+ 3.14.0
17
17
diff --git a/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java b/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java
index 4191570..883f512 100644
--- a/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java
+++ b/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java
@@ -29,172 +29,132 @@
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
-import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;
import software.amazon.lambda.powertools.metrics.Metrics;
-import software.amazon.lambda.powertools.metrics.MetricsUtils;
import software.amazon.lambda.powertools.tracing.Tracing;
import software.amazon.lambda.powertools.logging.CorrelationIdPathConstants;
import software.amazon.lambda.powertools.logging.Logging;
/**
- * Handler for requests to Lambda function.
+ * Handler for property search requests.
*/
public class PropertySearchFunction
implements RequestHandler {
private static final Logger logger = LogManager.getLogger(PropertySearchFunction.class);
+ private static final String APPROVED_STATUS = "APPROVED";
+ private static final String CONTENT_TYPE = "application/json";
- String TABLE_NAME = System.getenv("DYNAMODB_TABLE");
+ private final String tableName = System.getenv("DYNAMODB_TABLE");
+ private final DynamoDbAsyncTable propertyTable;
+ private final ObjectMapper objectMapper = new ObjectMapper();
- DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder()
- .httpClientBuilder(NettyNioAsyncHttpClient.builder())
- .build();
+ public PropertySearchFunction() {
+ DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder()
+ .httpClientBuilder(NettyNioAsyncHttpClient.builder())
+ .build();
- DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder()
- .dynamoDbClient(dynamodbClient)
- .build();
+ DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder()
+ .dynamoDbClient(dynamodbClient)
+ .build();
- DynamoDbAsyncTable propertyTable = enhancedClient.table(TABLE_NAME,
- TableSchema.fromBean(Property.class));
-
- final String SERVICE_NAME = System.getenv("POWERTOOLS_SERVICE_NAME");
- final String METRICS_NAMESPACE = System.getenv("POWERTOOLS_METRICS_NAMESPACE");
- final String EVENT_BUS = System.getenv("EVENT_BUS");
-
- MetricsLogger metricsLogger = MetricsUtils.metricsLogger();
- ObjectMapper objectMapper = new ObjectMapper();
+ this.propertyTable = enhancedClient.table(tableName, TableSchema.fromBean(Property.class));
+ }
@Tracing
@Metrics(captureColdStart = true)
@Logging(logEvent = true, correlationIdPath = CorrelationIdPathConstants.API_GATEWAY_REST)
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
+ try {
+ if (!"GET".equalsIgnoreCase(input.getHttpMethod())) {
+ return createErrorResponse(400, "Method not allowed");
+ }
+
+ Map pathParams = input.getPathParameters();
+ if (pathParams == null || pathParams.get("country") == null || pathParams.get("city") == null) {
+ return createErrorResponse(400, "Missing required path parameters");
+ }
- Map headers = new HashMap<>();
- headers.put("Content-Type", "application/json");
- headers.put("X-Custom-Header", "application/json");
- APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
- .withHeaders(headers);
- String method = input.getHttpMethod();
- if (!method.equalsIgnoreCase("get")) {
- return response
- .withStatusCode(400)
- .withBody("{ \"message\": \"ErrorInRequest\", \"requestdetails\": \"Input Invalid\" }");
+ String partitionKey = buildPartitionKey(pathParams.get("country"), pathParams.get("city"));
+ String sortKey = buildSortKey(input.getResource(), pathParams);
+
+ List properties = queryTable(partitionKey, sortKey);
+ String responseBody = objectMapper.writeValueAsString(properties);
+
+ return createSuccessResponse(responseBody);
+
+ } catch (Exception e) {
+ logger.error("Error processing request", e);
+ return createErrorResponse(500, "Internal server error");
}
- String requestPath = input.getResource();
- String responseString = null;
- String strPartitionKey = ("search#" + input.getPathParameters().get("country") + "#"
- + input.getPathParameters().get("city")).replace(' ', '-').toLowerCase();
+ }
- String strSortKey = null;
- switch (requestPath) {
+ private String buildPartitionKey(String country, String city) {
+ return ("search#" + country + "#" + city).replace(' ', '-').toLowerCase();
+ }
+
+ private String buildSortKey(String resource, Map pathParams) {
+ switch (resource) {
case "/search/{country}/{city}":
- // code to call
- logger.info("path is " + requestPath);
-
- try {
- List result = queryTable(strPartitionKey, null);
- responseString = objectMapper.writeValueAsString(result);
- } catch (Exception e) {
- return response
- .withStatusCode(500)
- .withBody(
- "{ \"message\": \"ErrorInRequest\", \"requestdetails\": \"Cannot Process Request\" }");
- }
- break;
+ return null;
case "/search/{country}/{city}/{street}":
- // code to call
- logger.info("path is " + requestPath);
- strSortKey = input.getPathParameters().get("street");
- strSortKey = strSortKey.replace(' ', '-').toLowerCase();
-
- try {
- List result = queryTable(strPartitionKey, strSortKey);
- responseString = objectMapper.writeValueAsString(result);
- } catch (Exception e) {
- return response
- .withStatusCode(500)
- .withBody(
- "{ \"message\": \"ErrorInRequest\", \"requestdetails\": \"Cannot Process Request\" }");
- }
- break;
+ return pathParams.get("street").replace(' ', '-').toLowerCase();
case "/properties/{country}/{city}/{street}/{number}":
- logger.info("path is " + requestPath);
- strSortKey = input.getPathParameters().get("street") + "#" + input.getPathParameters().get("number");
- strSortKey = strSortKey.replace(' ', '-').toLowerCase();
-
- try {
- List result = queryTable(strPartitionKey, strSortKey);
- responseString = objectMapper.writeValueAsString(result);
- } catch (Exception e) {
- return response
- .withStatusCode(500)
- .withBody(
- "{ \"message\": \"ErrorInRequest\", \"requestdetails\": \"Cannot Process Request\" }");
- }
- break;
+ return (pathParams.get("street") + "#" + pathParams.get("number")).replace(' ', '-').toLowerCase();
default:
- return response
- .withStatusCode(400)
- .withBody("{ \"message\": \"ErrorInRequest\", \"requestdetails\": \"Input Invalid\" }");
-
+ throw new IllegalArgumentException("Unsupported resource path: " + resource);
}
+ }
- return response
+ private APIGatewayProxyResponseEvent createSuccessResponse(String body) {
+ return new APIGatewayProxyResponseEvent()
.withStatusCode(200)
- .withBody(responseString);
+ .withHeaders(Map.of("Content-Type", CONTENT_TYPE))
+ .withBody(body);
}
- public List queryTable(String partitionkey, String sortKey) throws Exception {
+ private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) {
+ String errorBody = String.format("{\"error\":\"%s\"}", message);
+ return new APIGatewayProxyResponseEvent()
+ .withStatusCode(statusCode)
+ .withHeaders(Map.of("Content-Type", CONTENT_TYPE))
+ .withBody(errorBody);
+ }
- try {
- if (partitionkey == null) {
- throw new Exception("Invalid Input");
- }
- List result = new ArrayList();
- SdkPublisher properties = null;
-
- AttributeValue attributeValue = AttributeValue.builder()
- .s("APPROVED")
- .build();
- Map expressionValues = new HashMap<>();
- expressionValues.put(":value", attributeValue);
-
- Map expressionNames = new HashMap<>();
- expressionNames.put("#property_status", "status");
-
- Expression expression = Expression.builder()
- .expressionNames(expressionNames)
- .expression("#property_status = :value")
- .expressionValues(expressionValues)
- .build();
-
- if (sortKey != null) {
- Key key = Key.builder().partitionValue(partitionkey).sortValue(sortKey).build();
-
- QueryConditional queryConditional = QueryConditional.sortBeginsWith(key);
- QueryEnhancedRequest request = QueryEnhancedRequest.builder().queryConditional(queryConditional)
- .filterExpression(expression).build();
- properties = propertyTable.query(request).items();
-
- } else {
- Key key = Key.builder().partitionValue(partitionkey).build();
- QueryConditional queryConditional = QueryConditional.keyEqualTo(key);
- QueryEnhancedRequest request = QueryEnhancedRequest.builder().queryConditional(queryConditional)
- .filterExpression(expression).build();
- properties = propertyTable.query(request).items();
- }
+ private List queryTable(String partitionKey, String sortKey) throws Exception {
+ if (partitionKey == null) {
+ throw new IllegalArgumentException("Partition key cannot be null");
+ }
- CompletableFuture future = properties.subscribe(res -> {
- // Add response to the list
- result.add(res);
- });
- future.get();
+ List result = new ArrayList<>();
+
+ Expression filterExpression = Expression.builder()
+ .expressionNames(Map.of("#property_status", "status"))
+ .expression("#property_status = :value")
+ .expressionValues(Map.of(":value", AttributeValue.builder().s(APPROVED_STATUS).build()))
+ .build();
+
+ QueryConditional queryConditional;
+ if (sortKey != null) {
+ Key key = Key.builder().partitionValue(partitionKey).sortValue(sortKey).build();
+ queryConditional = QueryConditional.sortBeginsWith(key);
+ } else {
+ Key key = Key.builder().partitionValue(partitionKey).build();
+ queryConditional = QueryConditional.keyEqualTo(key);
+ }
- return result;
+ QueryEnhancedRequest request = QueryEnhancedRequest.builder()
+ .queryConditional(queryConditional)
+ .filterExpression(filterExpression)
+ .build();
+ try {
+ SdkPublisher properties = propertyTable.query(request).items();
+ CompletableFuture future = properties.subscribe(result::add);
+ future.get();
+ return result;
} catch (DynamoDbException | InterruptedException | ExecutionException e) {
- throw new Exception(e.getMessage());
+ logger.error("Error querying DynamoDB", e);
+ throw new Exception("Database query failed: " + e.getMessage());
}
}
-
}