From 3950827f5770c6c848093aba4988889b7c3edaf2 Mon Sep 17 00:00:00 2001 From: Steven Cook Date: Mon, 25 Aug 2025 08:45:53 +1000 Subject: [PATCH 1/3] Updated unicorn approvals dependencies and updated code. --- .gitignore | 1 + unicorn_approvals/ApprovalsService/pom.xml | 42 +++--- .../ContractStatusChangedHandlerFunction.java | 120 +++++++-------- .../PropertiesApprovalSyncFunction.java | 128 ++++++++-------- .../WaitForContractApprovalFunction.java | 142 ++++++++---------- .../java/approvals/dao/ContractStatus.java | 94 ++++++++---- .../java/approvals/ContractStatusTests.java | 73 +++++---- unicorn_contracts/ContractsService/pom.xml | 11 +- .../java/contracts/ContractEventHandler.java | 139 ++++++++--------- .../classes/publicationmanager/Address.class | Bin 1114 -> 0 bytes .../PublicationApprovedEventHandler.class | Bin 7567 -> 0 bytes .../publicationmanager/RequestApproval.class | Bin 990 -> 0 bytes .../RequestApprovalFunction.class | Bin 14455 -> 0 bytes .../AWSEvent.class | Bin 6635 -> 0 bytes .../PublicationEvaluationCompleted.class | Bin 2724 -> 0 bytes .../marshaller/Marshaller.class | Bin 2726 -> 0 bytes 16 files changed, 384 insertions(+), 366 deletions(-) delete mode 100644 unicorn_web/PublicationManagerService/target/classes/publicationmanager/Address.class delete mode 100644 unicorn_web/PublicationManagerService/target/classes/publicationmanager/PublicationApprovedEventHandler.class delete mode 100644 unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApproval.class delete mode 100644 unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApprovalFunction.class delete mode 100644 unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/AWSEvent.class delete mode 100644 unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted.class delete mode 100644 unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller.class 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..afe8ebf 100644 --- a/unicorn_contracts/ContractsService/pom.xml +++ b/unicorn_contracts/ContractsService/pom.xml @@ -44,17 +44,17 @@ com.fasterxml.jackson.core jackson-databind - 2.15.2 + 2.17.2 com.fasterxml.jackson.core jackson-core - 2.15.2 + 2.17.2 com.fasterxml.jackson.core jackson-annotations - 2.15.2 + 2.17.2 @@ -179,9 +179,10 @@ org.apache.maven.plugins maven-compiler-plugin + 3.11.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..842ab2e 100644 --- a/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java +++ b/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java @@ -16,52 +16,38 @@ import software.amazon.lambda.powertools.metrics.MetricsUtils; import software.amazon.lambda.powertools.tracing.Tracing; -import java.util.Date; +import java.time.Instant; import java.util.HashMap; import java.util.Map; 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); - DynamoDbClient dynamodbClient = DynamoDbClient.builder() - .build(); - - Logger logger = LogManager.getLogger(); - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); + private DynamoDbClient dynamodbClient = DynamoDbClient.builder().build(); + private final MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); @Override public Void handleRequest(SQSEvent event, Context context) { - for (SQSMessage msg : event.getRecords()) { - // cehck in message attributes about the http method (HttpMethod) - logger.debug(msg.toString()); + LOGGER.debug("Processing message: {}", msg.getMessageId()); + String httpMethod = msg.getMessageAttributes().get("HttpMethod").getStringValue(); - if ("POST".equalsIgnoreCase(httpMethod)) { - try { + try { + if ("POST".equalsIgnoreCase(httpMethod)) { 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 + LOGGER.info("Contract created successfully"); + } else if ("PUT".equalsIgnoreCase(httpMethod)) { updateContract(msg.getBody()); - } catch (JsonProcessingException jsonException) { - logger.error("Unknown Exception occoured: " + jsonException.getMessage()); - logger.fatal(jsonException); - jsonException.printStackTrace(); + LOGGER.info("Contract updated successfully"); } - + } catch (JsonProcessingException e) { + LOGGER.error("JSON processing error for message {}: {}", msg.getMessageId(), e.getMessage()); + throw new RuntimeException("Failed to process contract", e); } - } return null; } @@ -69,82 +55,83 @@ public Void handleRequest(SQSEvent event, Context context) { @Tracing private void createContract(String strContract) throws JsonProcessingException { String contractId = UUID.randomUUID().toString(); - Long createDate = new Date().getTime(); - Contract contract = objectMapper.readValue(strContract, Contract.class); + long createDate = Instant.now().toEpochMilli(); + Contract contract = OBJECT_MAPPER.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()); + 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() + ); - HashMap itemValues = new HashMap<>(); + Map 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_created", AttributeValue.builder().n(String.valueOf(createDate)).build()); + itemValues.put("contract_last_modified_on", AttributeValue.builder().n(String.valueOf(createDate)).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()); + Map address = Map.of( + "country", AttributeValue.builder().s(contract.getAddress().getCountry()).build(), + "city", AttributeValue.builder().s(contract.getAddress().getCity()).build(), + "street", AttributeValue.builder().s(contract.getAddress().getStreet()).build(), + "number", AttributeValue.builder().n(String.valueOf(contract.getAddress().getNumber())).build() + ); - itemValues.put("address", - AttributeValue.builder().m(address).build()); - PutItemRequest putItemRequest = PutItemRequest.builder().tableName(DDB_TABLE) + 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)") + .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); + } catch (ConditionalCheckFailedException e) { + LOGGER.error("Unable to create contract for Property '{}'. Active contract already exists", + contract.getPropertyId()); + throw new RuntimeException("Contract already exists for property", 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()); + Contract contract = OBJECT_MAPPER.readValue(strContract, Contract.class); + LOGGER.info("Updating contract for Property ID: {}", contract.getPropertyId()); + + Map itemKey = Map.of( + "property_id", AttributeValue.builder().s(contract.getPropertyId()).build() + ); + + Map expressionAttributeValues = 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") + .updateExpression("set contract_status=:approved, modified_date=:modifiedDate") .expressionAttributeValues(expressionAttributeValues) - .conditionExpression( - "attribute_exists(property_id) AND contract_status IN (:draft)") + .conditionExpression("attribute_exists(property_id) AND contract_status IN (: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("Unable to update contract for Property '{}'. Status is not DRAFT", contract.getPropertyId()); + throw new RuntimeException("Contract not in valid state for update", e); + } catch (ResourceNotFoundException e) { + LOGGER.error("Contract not found for Property '{}'", contract.getPropertyId()); + throw new RuntimeException("Contract not found", e); } } public void setDynamodbClient(DynamoDbClient dynamodbClient) { this.dynamodbClient = dynamodbClient; } - } 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 a3589f574f05b94ea890af1e28e7206a346a0d61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1114 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

--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 C@;O=n 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 5087c6df352a2072339219cf01cec9d0be954643..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 990 zcmaJ=%Wl&^6g`uq33XgrN?Qt)v_KbWk?;oql_I1{pa@WlvPBbT2os!fu*Vhr6-Xc? z7Hn971QHAQ9K;zXZE0${@Yr|ep4YYg?a!yr0PbP4hLXUIQPz!gD6NjoP#W1+NwBA0 zWh%9;(I|-zWK=^%U|}E+WDrTy4<2_1DzpL~k4Gx8Zyxjn7Td=S`!><0e@Ec*US_Nw zs*X-|H&QKQVw)?a0zo^Bhrx51T9v#WM!`UaFVolr@~A0z$ok1dgKRVOk|}}uq3$zH z;hOCA5|xs1tzD3%H9jT0dRH55cLd7Kt&TwDe%wJ*Zi5R6b@q+o?tiI*pzu>Qh;48Q~0q?Ne*{yEh`heAAX)oXX&KN&(hLv&GMBZ5&2joa_t-wfQ g+Q`pBBOAD0WTY`|+VaxSING-q%hT|v;B#9RJY@1DygUR%|<4?&e)nYEW0_K&Y0Wu#ENVx zDn6@J&Q!Za-=>EXdTMicZT}V{YBNdUVhTi@`W%;d%O&aHpKo z#8jO!nZ7!n^U@-%@|B47Jpsl{FHn5*ckwCT8pywfrV?CpBS2;UFYAoyX1LqMNJ?>*u?;M2$;4xujXd#f zrS>SWQ!eV4i^A>W0E$W1OsS}A^fVa2)Yus{li>l~g5-B36X7j-bjX6JV!EyO$5Sy^ zubt_jv~pQI6}Oi&mDMfmV=8YoV}_r~Xr4yL(eZwoPbaH%614R|%D=`jEsYjX9h7LZ zVLKLqWpxYR)NHB-Jdi%ioo4*PO}d!K zUt7&Y0-gabqiJEEz`aQ$m3%%r2ljtqDjqd6sm1G!e(~vCjjBoWQ8OsKSZ}hf;4VFR~(s5xCjV=jZ|Kg>&Xmyj=YOO{ysn$mqz;N3(z(1^QYuO~7g`axpLX~>+ zymnv?L*D6U5% zo%+Fy{%kxE6GpNUT6rh{TO2^pkr)lABpgl&aD%p;4yWQ~IAz%OaJWnS?TVYygjeC4 z;|v6Xo0&RFqct@+^Ye&RQ=EoW+5$Z)fmp%G9LQ(8;51555{4;|a1eGsFzu`>#Yl;< zF8qJECpNSn!39K1$^lkngrhR@g!>(K7vL1(&?IOzHA>S~rV2Tg=?HLg|4=TK8qQ!g z9Z^kL+O87xv#kVsrWJsM_;xULmlof&z!jKNO;^yBD!rAd6q64Mu10U8s|xYGtq|XD zEGf$=DGOZa;b3MF^;ZkIzJqDiVPdV+fp=;&hvusEE@VvJNL@|up=(vThUxSv6Cx9a z9%O_`U8C#hy@*Hy88g{p=t#SIz0R*Sp;Nxw=Mcxj#d*~RtKm|-wgs(d_I*N`~>29WmY2;&mx{KzC^&ipb7&=x|5b>1J zl}+{=8OdQ$1(;Di(WhtPLiFxnxjh)Sm=<+Tz1%_SSA%3cmN49Ui0SD%nL9eUJyQGe zygs2P`(t`IlTF$2q!E@9v%<~Y9V>O)*sc#F*WEGf!jXw;(-a}Knkn1ZVZ$s0mkxes zD{ZDM!`WICMLVKKT4ZRJO7}A@nOp=i#GH@h80e#Vu~2dq79!_c=TO@hx8mS*b1G%p zvW^6!Pn%#|E{D?Qc2psznXtlkMvsaLvDbNm-&0wYG*J3Qt?(M>iB+{xJuM1Gq@A%I zThG{KsBd{{(DGHP$i_uM3Gaj<$tHwz(4R39lBSLr3c^7U9nLt_u`8JT&yJW%BNI|{ z6cK)Ugy#8Z6s}Y*N!g9P!)e1udx1F|SiblUZvmgH=}~%IrN^Lp<7q2^X!HapgjlMzugTxrwdW#tw-al`0 zBZ=@z-_q#Y^d0C=25jxk7$W8M3d)`8*zp0XPhoCdC^4IS^gOtiFWWc8V}ANR{XnJv zWm+(~xfUweJPeI~C{QW`_G0mmHTsF526bL9e2S=%ex}h2^m7oH)NRCQWd5glk!~H- zGd)7z(UX9v#&yyuhSoTXkmbKfzf|d^NvbV(iblVp|0`?*ML;pVF#tP#S)<>Gdg#cc zJ_HL+p%b+p3WGPx)>F16ROxpb{hs~+)(TZ}#IVVWWf5x+kBjuvpXkpjy^^u2hG`Ft9Yi;f{aOElbt44pLzay0tsKYtgSxfW}m@fAckY8~) zoxG3!$;4qlHPY)E{hR)SWXOWIfdg6K-8;`bZ^iG91%wxcd>D@+r{jM2IGsF{i9g;#FVSzo_P!JWJ(TNM~HJ#TShOJR6-JD}Dt~g{F4+cn(<5Wri$6&qN19 z17vd?B;*^i2aS*$9*kIiZagM7Iuf?XZ(*8KtShej_R}stO5>vi-J&_< zXi7C7%g3vHTv0SYw@2d;p8#`+A~(?EDOl4SF9-7d8rWAEy)3vkU*nVbWPrAeRLl)- z^XiJ5aFa;G$LLiB+A|q5BWQJKN0jt&J=1XuGKQ5+*b5?|1^G^K$jxw!*aDT&sG2=q zVS4h0ig-%KvGWfxr5X3q2;1o6$V7=LJVmf@F*38l+Cdo!!AEaDT0|%5&YjeWY4sEV z8=?_8N%B{mg84G0X$wcsH1(JO#m@(Xj@V>f>%2#XvBN#ZF^4z8l%(N1HU_CoV(OBC zNar#LaplptjYp+rBZ|RYRHkHl4K2h-H}Wf_jjIjJ@Z)!nnX#Q8E@E*5e600^3TbO# zWFB#tTe0Q=M=)WI$6^c00s+h|PUn$2xhee_o;@I|Nlphm5FOfOr#a+VCvcmj8C?VW ziw*&(+)CvxbO)!FQMgK_@-n6sUZZpon26H;ZfOEKK3)gbcci2|Lmk3){M^93=)CfU z``Y{0vHQiOLtFASzMWO>CP$$fz>;QV|NKQ3GRHp7Ah!|uulYFrt>G^^C4hpXgnbHsj{Hjatnd?Ao?sEXF6q? z#5Yy*5GPempw&HPUgJ#3NQ5sCAr1-^lf>!?jZ7?=F3mQi@YWa5TP# zuLW&!ob@0QE-9_mvb$2~|BoXQz&ZPSHNKv25T#mFG-{C5xK2)ba~V>-fn&fuq7Orx ztZY(J^FDsR$~Q4BE+MEA+iQF?-y*zUw%>|o;{9;2E4_P9+{%kc6ct<+O_P)jRFdh0 zVT&f*imd3*Kp{WCA5s~O%9F~7ci4v`!CksGf;BMO33`A(+uO3+s5 zb{5WV9E;1VVj;m$84bwe#?w?$iDfd`v~9#ZbM8gWG%3dIJB%nKVx|g{Q7{$a(~wA# zRN<}>#UM*3VMYv@&>VBG36u~CvY;{vYjLTC6Ezb%Vp;Rl7nE>GAi%9?nA#5t+Wuw= zMhr-OVDjqKf%E{BoL6e^5C^JbELW;}+hQRUAu&3p6Sv7a9l-LGBz zu*RPk3R?k%?TBHRM>Nife67YZ>~8nw1Db}6#^6zt0B(-Ope~l5_c#FeY5b@FC^}KC z$j|YCAJ_N^0a67>IfjkDpz#;;mePtU6&byl`X!COET;O!RCgG&pV9a$V)iUR%_HSB z)5W5%Y5c5MgtF1jk3Ps>*Z3QZH3F22CDZYBlXceJ&$tDE4jF$-<8SkKP*HBrz&Xmy zs@ZZf*8Psb^A+sGY}wDx^Y>N$9-PvfnZ7K;+`A!;a(8EKS^QdV;?XX+BeD;))Ot$$ z>4GzvBE)>EfQa%#{;|qGLT+=Ikj;bE_$T~RxEhI3OV;Yrlkn5VbQ0l1qUr^We=ZQH zw9ITKY6wDK)c7U-C6onfD+dw$28!;HvP<-bhxtZs6x6AJfN zrmn-HaCwD*uKk_=sq#Ni4jsC#<)Lc)FMb{V1>rm+>-w12V|%ap90r-@Hx;Bn1au3( zp(*J5L8VagI)=-XDw!)zLw$-`*#`;*B4ts^G^JczohaX8#&Kp{A&rh{{$w7{)wsL@ zB5)mlXgsZpr?#0xM%eK{t@4y%g&ws{;r@T4pe=iw*LAwH9e}Ik z5wJDC(=>;&>86Px$um;4P&k-LrhA)jlf*d}M5~@i^dL$$&8$?8@F_>5WgQ13Z={t_yk}f8|O2r1%Bm3eHsdhY+9(+3no(MLzPUMUrQmLl{v#@(-8>CD@&1(%m;U#+qby?14irp!=g zLW~Vav-a<~W71++en%>Wa}II+#jsRm2`*qxVD4nfs4Axy6)#1&yLyWl?PxAMk$VS2 zRn971<4);PnyN%f_wWt~K~>NgZ7&g_c_#48FgXP$rG5Toz9y+0ZlujI>h4uTWVMF<2)VQH65bhbJQ}@uBBNV9*EE}Qa z^?~z7sHHy8HbN`v>mQ+(QCc-ZUG;(PQCc@b7u5$g%9o1)?9x$+j?th(Pf~o05Qd(l z%SI@JFE&HDG{AQ<+wInO{dX3I-TlpPk1pc#oL*5E}c!QC_)#| zGP;n?q04DG4bgdIQZwzK7P^|+=vr#WHLN!JAg!c3F#8iYao$aNz4A3=)U-WT& z0R!oi^eIU6KD?LX*QepQ0lH9zYiq107#Bn-}8KZm{%mdJ)Hl4LH_6N<8^!o+p2oOZ|A5 z7C|b#u(u0wVYCl+u%1q$i)bO94YZNYqKhH z;GJa9H8cP{*-W?6Afz70&O^97lE8(EBz>1s=mMFzVUVPkXe+%!8CJ;RaJ>Vb+j#}e0&M|a$*ZWAJHYq31w79Op3eN$_|}PUw^PL%U~d(?kxI|1yhf!*ReDOL z@2k|PLZ}3FOw+8A-&I~KXJWGFKb}b-c&SRe-T-;E(_0NxxM4PqqH%f;gk2Mda!FUs z-NKG}C%%Z^I~txQbhLd{Gxi%13!aLyj+7l;c>AHw)_> zrQQBK2dIh*6RMpPKaO+j^xuYy8DQgbtzL?W1^80JWPx2Jx2x_8KU{ z$Fcij{NssF@F$^w;-6{^^QWMIEwqsD<4;2YqfpxW`2paSAWf=CnN*VyU^viL7odFx z(46_7#VVmN_}45x|KDgmL<9-prU2#^J7GVta-1+pNu{y7ij}+f@q-Y{?lp@bk%w~p z$UfeK-(y{k_`NsBkBR4#Ietn!pU&}D7lGB!K{)q9df$wcyHa&z_+5-&JjOpzD6njd zf2NQVHwEOLG5&=@v-wwJ{A-1RkMf^`6=VE2G5&8c{;Krl z9}ZTI@qZM`Da!g_`EKA`CMDS=+yJQb!2oONeuVi4K-p&y;y#Pu^dPvr8zJst1g%GC z4Z>$1+?(&R1pyG$LD*!8?R4Z^TQg%WoSf2?<3Z7N<4G!(& zQCkR#aL3jMFEAcqtCU*!5M)crEPPdzfFg*WPZdfK8R}8;?=kZ4aq@3Ssl(iQrBMkh zr(k9a`IJ+Y)08tPhzs{wWocyEFOGeDB_s_G)*>;~9s{ zWOvUwckempv*+G>l>GI-@4OG-9=sMolZL&O{Gz#RWU56gUn&<*8Y?U1(yCFYWLBzk z1uJjZR;g%GQFWg3rR9}^X`Azzu@l(?t7g%TphZL1lCf%J3Py1ubN}3unYT5x&6~Di z6*R;r{l*au?OAJ~XxP=VsiAplikj<=SBti_Y))GhYp!696^kX?aaqxjnWW#$!$!q6 z%V(AgnI$8Cs!}RujQXI=eUy)uODksCKFir!MZ}ytCZm`-yFx*YBx{$g;sQr*k|Ty~ zXGnY5tdy$dyh*QJGVrQx6*7}n#pd89Yo3|O5gJ-Vyp7|=4cp|nwz9cEZW>8r0E^YlL-3$5yG^sbiab+NEPVIy7vaw2J1D>hhdfo-znoASvaI!n9Gg zgzPF?>_vgqGPO4HUDMVQKzv&}Z*aW~osvyM|S zg@e0mU)4Ds9py+24`D6>L&Fy5Y;-=joT#EBhrEWQy7pWj!5t$tqd_atpYe!1jWu*+ zZJv&YjTLvFjV>CMMRCM9=gow0y`}jT!1SE=OJo;|;&IO6O)oZ?-F;C! z$-~UGK1C)%Y0V4i88(D#Y{HGSMQPJ;C+^~V(&62X!!bt>v<|(A9?zYa3e4Qg zqcxC+(8xS?W28Rqs59*hWf5STlZVGqrg0mCy$ajU5Z1Mn=E(kgv45EZoO1*wcqwAzJhJEgIM^!q#emZ*@ z?3fa)O2;2;jSe?i?5{H!4>Cyvn2giS0mY5VM6UKQCI@hD2$O`L$;3LD9K>hBm?Zp6 z4l~#zO0X&uf3O~tEo;|Fe}Ks`x;d`6QJKh%E{w@UXr1)?nM|*f$$jB<((h+7!(eBX zU{xmmU_B=BwV4bAm^?@~Clxm;6S>ESF*y{83 zq#K9pYv)LC?IZ(CPSH(4aicPk$6FYa`*Ad6?Iit7R@TYn7)w=dPYwG?($Bm(Ima)xfsDsEIJ@?;NVk`1ksl%L7DbuyU>ualIY$rB9rNhMg7i9cA6 z$+oqbqytQzrkiIJH!2g^)Pym4Ae2ek&*ZsvGMNr%lJ+xsfx*701gkRf2kSA}zBZHL z0F(1{^DV`V%0%{aVN6bhG8y(Wd3l{oJ{!(N>G)))@(P1}RS8yQ;t$qilEaK#nVM*~ zMDu-dDE&HGTP~$PMCZ^I%%=JGr)X`yl>PwPVbOc-s`(N_Y;hXhJ!~X^ zA2RJ9u}^%Bt@lsZl=cwcZ{jQ;zl&n-Ev!tYbZime2kxy z>c?DlZ=?DNSDk27f7w;{H>#g@)dP*{XI*u&QT?7cx337VL#AstUZ)1UqeP6r|4!L}VgNW~M- z>&!?0)bM=iez9Tc4~^Dmo@47qZnqTSt(zUoHdb7U^qS8EIG z*qW@GoMqlKZPQ;6NY2bQ1yWU~Eptd>UmoMwGXjDAdE_yw{$9zWfKh>+b<>vDyBllL zy%kbas5>pgY8s@CnM8{zf8As%ck3^wkj&Yd8RT{L<%KIgkbYlLTeTgZnH@wSQ1pgv z-ZKm_c^{?gtzxPojq-GSo{^t-DSd%$4npnD&E8WLtK@MM#{{x;RkaL{n~U3^1&ebz z=s2n21eFqKvg?~xbP2pXX7HxKkvPaGMl8i%RNez;yS5{fz`@uZ?NipfgiNsEQ_@RD zClX6`-SF0x_yQ%?6w0%C%;7z1QkeuMX6_FGQDefs6W%Ee=LGgXf732@O{*>492T%I zi;GOJrPwLGxJ{J4&Z!fNd0fI}UK4g7X|piL;@m`6d9eOb%(*PC5_pME|5k>&Ia|u{ zMqSI}ebm`9e5Yo&rOoyg`V@F09%zg57y(i2EUvQ*xh$6HMFrZr-L>}FQWiI9{6-~m zE6>JySRmo8vGWZ0=>xt?!tJhQDM{VR;yyb|PWjE@Bdlupm|>o0IFCBPpO%ptGi5msKJIhb8w=90aq#CAv3fv#y zSU`&72***5yBKH$2f30DkKt96I1`*Jn9Aijx=iq^l`G2K6aFTkTq(3bk$H>}K!wZ%NcuU2Kn@Vd!5ENC z4@jm5q%b_Ise!Bx(b;s&nbvcrMTMaFhh{bPd{#$>W_4tER+DXH%R_1v8#W zKBj|YP~mB66F5N#Ct1z6D2G$nhj(au7Zsew8Ge;`N@36*NAui!ijWS_@;2WD&nP{o zv@!87rZt)bkDlhJ6n36!RHB$j|BmEJQo(Diq!g~kGn`RyLcxV9nUC=~(NhWgM&#{V zf>A1|{f32=r#N4)6fSI{dZ$vj!cSRw-$aeSg{5%$MzlP&effjHx`{@=p!<=b<0@HM z#B*msQQQEv9NkQ}jXV?s|xY^Cw7i|3r z{u-_4q#k?p_}L%j>7Ch-(2yF7%{j?VX6D}KxzD{bcmMw9_dfyL!M7R)7>d5RFY4R~ z9m{k*XPY+~o_ok`-)Mw8wq^3bavebvp&FTPywTW7ddAP>ea_XFWQuG_`|H^28?$Efju?^sT)Wlv@ zXH462tYDcTHkaC97+i5{A_;~wI&{3nFm8H+2O>g>>==x>)N27H$l<7tA;jrfupChe z>pQ}$@*P_cM8P$=y}><8{%=jjGm2LdF`U;ij$z^!>{~uVx$s6cB^9)m*bEDE1*NX# z8s#u(gh3^s@aySl_L$~rDONJ~*c1(!L0`k$4Cjt)SIg}chD62Mb9fMX6zYvkb?Kz9 zS~S_Z}(&0+qH_}k&X>)QbVX&e$<S z$Xl`_j&@r9by>BzObs+fuN4%#?4mS^V;CBJ>2jlgSLi7}+HXkyijiMvIDoVC8&?Y> z7*jLS0kzVWkwH&sHZ=bX=ekXFWuiwLFo8+hn7{>GBp#P&JV+~3ct^Q?S8=G&%7EPU z6eIHsO-xIdF~uUO7RCs9T)CZ$HsXDJKzpR)5AhK>yh7s$QqIiLOadv)_kdhbkmWAO zt3Q*22r>$M;w3l(A4@bcUBrzpxJBGlaG&5-54cYiTtETHEX-1bP24WZK(CIc2mipG zt=K}Pi7%SS{e^UgMHKiY%G8u%G8@_Bh{^Ww(p5Z|g51*pcL{&mY86T(A0Mi6HL#DH&|FnXZh78}ReOoU8x!LMp From 84f0bee36ae9c9baaa39c1bb5fe3533c172cf078 Mon Sep 17 00:00:00 2001 From: Steven Cook Date: Mon, 25 Aug 2025 09:28:24 +1000 Subject: [PATCH 2/3] Updated Unicorn Contracts. --- unicorn_contracts/ContractsService/pom.xml | 49 +++-- .../java/contracts/ContractEventHandler.java | 186 ++++++++++++------ .../main/java/contracts/utils/Address.java | 61 +++++- .../main/java/contracts/utils/Contract.java | 69 +++++-- .../utils/ContractStatusChangedEvent.java | 47 ++++- .../java/contracts/utils/ResponseParser.java | 66 +++++-- .../java/contracts/CreateContractTests.java | 107 ++++++++-- 7 files changed, 444 insertions(+), 141 deletions(-) diff --git a/unicorn_contracts/ContractsService/pom.xml b/unicorn_contracts/ContractsService/pom.xml index afe8ebf..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.17.2 + 2.18.4 com.fasterxml.jackson.core jackson-core - 2.17.2 + 2.18.4 com.fasterxml.jackson.core jackson-annotations - 2.17.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,7 +194,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.14.0 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 842ab2e..c1f2918 100644 --- a/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java +++ b/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java @@ -12,13 +12,11 @@ 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.time.Instant; -import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; public class ContractEventHandler implements RequestHandler { @@ -26,37 +24,86 @@ public class ContractEventHandler implements RequestHandler { 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"; - private DynamoDbClient dynamodbClient = DynamoDbClient.builder().build(); - private final MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); + private final DynamoDbClient dynamodbClient; + + 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()) { - LOGGER.debug("Processing message: {}", msg.getMessageId()); + processMessage(msg); + } + return null; + } + + private void processMessage(SQSMessage msg) { + LOGGER.debug("Processing message: {}", msg.getMessageId()); + + try { + String httpMethod = extractHttpMethod(msg); + String body = msg.getBody(); - String httpMethod = msg.getMessageAttributes().get("HttpMethod").getStringValue(); - try { - if ("POST".equalsIgnoreCase(httpMethod)) { - createContract(msg.getBody()); - LOGGER.info("Contract created successfully"); - } else if ("PUT".equalsIgnoreCase(httpMethod)) { - updateContract(msg.getBody()); - LOGGER.info("Contract updated successfully"); - } - } catch (JsonProcessingException e) { - LOGGER.error("JSON processing error for message {}: {}", msg.getMessageId(), e.getMessage()); - throw new RuntimeException("Failed to process contract", e); + 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 = Instant.now().toEpochMilli(); - Contract contract = OBJECT_MAPPER.readValue(strContract, Contract.class); + 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(), @@ -64,49 +111,33 @@ private void createContract(String strContract) throws JsonProcessingException { ":expired", AttributeValue.builder().s(ContractStatusEnum.EXPIRED.name()).build() ); - Map 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(String.valueOf(createDate)).build()); - itemValues.put("contract_last_modified_on", AttributeValue.builder().n(String.valueOf(createDate)).build()); - itemValues.put("contract_id", AttributeValue.builder().s(contractId).build()); - itemValues.put("contract_status", AttributeValue.builder().s(ContractStatusEnum.DRAFT.name()).build()); - - Map address = Map.of( - "country", AttributeValue.builder().s(contract.getAddress().getCountry()).build(), - "city", AttributeValue.builder().s(contract.getAddress().getCity()).build(), - "street", AttributeValue.builder().s(contract.getAddress().getStreet()).build(), - "number", AttributeValue.builder().n(String.valueOf(contract.getAddress().getNumber())).build() - ); - - itemValues.put("address", AttributeValue.builder().m(address).build()); - - PutItemRequest putItemRequest = PutItemRequest.builder() + PutItemRequest request = PutItemRequest.builder() .tableName(DDB_TABLE) - .item(itemValues) - .conditionExpression("attribute_not_exists(property_id) OR contract_status IN (:cancelled , :closed, :expired)") + .item(item) + .conditionExpression("attribute_not_exists(property_id) OR contract_status IN (:cancelled, :closed, :expired)") .expressionAttributeValues(expressionValues) .build(); - + try { - dynamodbClient.putItem(putItemRequest); + dynamodbClient.putItem(request); } catch (ConditionalCheckFailedException e) { - LOGGER.error("Unable to create contract for Property '{}'. Active contract already exists", - contract.getPropertyId()); - throw new RuntimeException("Contract already exists for property", 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 = OBJECT_MAPPER.readValue(strContract, Contract.class); - LOGGER.info("Updating contract for Property ID: {}", contract.getPropertyId()); + private void updateContract(String contractJson) throws JsonProcessingException { + Contract contract = OBJECT_MAPPER.readValue(contractJson, Contract.class); + validateContractForUpdate(contract); - Map itemKey = Map.of( + LOGGER.info("Updating contract for Property ID: {}", contract.getPropertyId()); + + Map key = Map.of( "property_id", AttributeValue.builder().s(contract.getPropertyId()).build() ); - Map expressionAttributeValues = Map.of( + 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() @@ -114,24 +145,53 @@ private void updateContract(String strContract) throws JsonProcessingException { UpdateItemRequest request = UpdateItemRequest.builder() .tableName(DDB_TABLE) - .key(itemKey) - .updateExpression("set contract_status=:approved, modified_date=:modifiedDate") - .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 e) { - LOGGER.error("Unable to update contract for Property '{}'. Status is not DRAFT", contract.getPropertyId()); - throw new RuntimeException("Contract not in valid state for update", 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 RuntimeException("Contract not found", 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; + } } From 5eb0b0349dc59b0b8f9b3109d6e8c8a8cadc9298 Mon Sep 17 00:00:00 2001 From: Steven Cook Date: Mon, 25 Aug 2025 10:22:14 +1000 Subject: [PATCH 3/3] Updated unicorn web. --- unicorn_web/Common/pom.xml | 4 +- .../Common/src/main/java/dao/Property.java | 74 ++++-- unicorn_web/PublicationManagerService/pom.xml | 46 ++-- .../PublicationApprovedEventHandler.java | 149 +++++++----- .../RequestApprovalFunction.java | 230 +++++++++--------- unicorn_web/SearchService/pom.xml | 46 ++-- .../java/search/PropertySearchFunction.java | 218 +++++++---------- 7 files changed, 403 insertions(+), 364 deletions(-) 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/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()); } } - }