From b22c8bc32f6157a1c16cdda5c47c59484c36261d Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Sat, 11 Dec 2021 16:34:05 +0200
Subject: [PATCH 01/21] docs: add Kevinyl3 as a contributor for review (#1926)
* docs: update README.md [skip ci]
* docs: update .all-contributorsrc [skip ci]
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 925f8fc36..3aaf8696a 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1730,6 +1730,15 @@
"contributions": [
"doc"
]
+ },
+ {
+ "login": "Kevinyl3",
+ "name": "Kevin",
+ "avatar_url": "https://avatars.githubusercontent.com/u/47126749?v=4",
+ "profile": "http://no website",
+ "contributions": [
+ "review"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 2701103c4..ce4578984 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
[](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns)
[](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-[](#contributors-)
+[](#contributors-)
@@ -320,6 +320,7 @@ This project is licensed under the terms of the MIT license.
 Abhinav Vashisth đź“– |
+  Kevin đź‘€ |
From 9063336687c06fe1100742d8b2c149cc2dbb13cd Mon Sep 17 00:00:00 2001
From: Shrirang
Date: Sat, 11 Dec 2021 20:29:36 +0530
Subject: [PATCH 02/21] feature: Claim check pattern azure (#1897)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Archietecture Diagram added
* Added pom.xml
* Architecture Diagram Updated
* Added Microservices and kafka
* ReadME File Added
* ReadME file Updated
* #1329 ReadMe file updated and working pattern code added
* #1329 readme file updated
* #1329 readme file updated and java documentation added
* #1329 repository merged
* Update claim-check-pattern/ReadME.md
#1329 Real world description updated
Co-authored-by: Subhrodip Mohanta
* #1329 Update claim-check-pattern/ReadME.md
Co-authored-by: Subhrodip Mohanta
* #1329 Applicability section in ReadMe file updated
Co-authored-by: Subhrodip Mohanta
* #1329 Tutorial section in ReadMe file updated
Co-authored-by: Subhrodip Mohanta
* #1329 Storage Data section in ReadMe File updated
Co-authored-by: Subhrodip Mohanta
* #1329 workflow section Update claim-check-pattern/ReadME.md
Co-authored-by: Subhrodip Mohanta
* #1329 Update claim-check-pattern/pom.xml
Co-authored-by: Subhrodip Mohanta
* #1329 deleted mvnw.cmd file
* #1329 deleted drawio file
* #1329 deleted mvnw file
* #1329 deleted mvnw file
* #1329 mvnw.cmd file deleted
* #1329 Update claim-check-pattern/usage-cost-processor/src/main/java/com/callusage/domain/MessageHeader.java
Co-authored-by: Subhrodip Mohanta
* #1329 Update claim-check-pattern/usage-cost-processor/src/main/java/com/callusage/domain/UsageCostDetail.java
Co-authored-by: Subhrodip Mohanta
* #1329 Update claim-check-pattern/usage-cost-processor/src/main/java/com/callusage/domain/UsageDetail.java
Co-authored-by: Subhrodip Mohanta
* #1329 deleted mvnw file
* #1329 deleted mvnw.cmd file
* #1329 Update claim-check-pattern/usage-detail-sender/src/main/java/com/callusage/domain/Message.java
Co-authored-by: Subhrodip Mohanta
* #1329 Update claim-check-pattern/ReadME.md
Co-authored-by: Subhrodip Mohanta
* #1329 pom file dependencies fixed, readmeflie updated, removed unused imports
* #1329 Readfile updated, class javadoc added
* #1329 UML class diagrams added, readme file updated
* #1329 lombok annotations used on model classes, common dependencies moved to parent pom
* #1329 test cases added
* include claim-check-pattern in parent pom
* #1329 code smells fixed
* #1329 code smells removed
* #1329 security issues fixed
* #1329 updated pom files and refactored packages name
* #1329 checkstyle warning fixed
* #1329 code coverage increased
* #1329 minor changed.
* checkpoint created with common utility
* Claim-Check-Pattern | Shrirang97 | Implemented using Java Azure Functions
* Claim-Check-Pattern | Shrirang97 | Updated Functions logic
* Update MessageHandlerUtility.java
* Update UsageCostProcessorFunction.java
* claim-check-pattern | Shrirang97 | Added test cases
* claim-check-pattern | Shrirang97 | Test cases for Azure functions fixed
* Claim-Check-Pattern | Shrirang | Used string as request body
* claim-check-pattern | Shrirang | Working test cases
* claim-check-pattern | Shrirang | Issue fixed while deserializing
* claim-check-pattern | Shrirang | removed unused import
* claim-check-pattern | Shrirang | fixed refactoring
* claim-chek-pattern | Shrirang | added lombok | fixed dependencies & test cases
* Delete .DS_Store
* claim-check-pattern | Shrirang | Fixed unrelated file
* Update BookService.java
* Update BookService.java
* claim-check-pattern | Shrirang | Fixed unrelated files
* claim-check-pattern | Shrirang | Fixed review comments
* Update UsageCostProcessorFunction.java
* Update ReadME.md
* Rename ReadME.md to README.md
* claim-check-pattern | Shrirang | Incorporated review comments.
* Update cloud-claim-check-pattern/README.md
Co-authored-by: Ilkka Seppälä
* Update cloud-claim-check-pattern/README.md
Co-authored-by: Ilkka Seppälä
* Update cloud-claim-check-pattern/README.md
Co-authored-by: Ilkka Seppälä
* Update cloud-claim-check-pattern/README.md
Co-authored-by: Ilkka Seppälä
* Update cloud-claim-check-pattern/README.md
Co-authored-by: Ilkka Seppälä
* Update cloud-claim-check-pattern/README.md
Co-authored-by: Ilkka Seppälä
* Update cloud-claim-check-pattern/README.md
Co-authored-by: Ilkka Seppälä
* Updated readme file
* Read me file updated | Added more description
Co-authored-by: Subhrodip Mohanta
Co-authored-by: Subhrodip Mohanta
Co-authored-by: Ilkka Seppälä
---
cloud-claim-check-pattern/.gitignore | 38 ++++
cloud-claim-check-pattern/README.md | 82 ++++++++
.../call-usage-app/.gitignore | 38 ++++
.../etc/call-usage-app.urm.puml | 117 +++++++++++
.../call-usage-app/host.json | 7 +
.../call-usage-app/pom.xml | 150 +++++++++++++++
.../functions/UsageCostProcessorFunction.java | 168 ++++++++++++++++
.../claimcheckpattern/domain/Message.java | 43 +++++
.../claimcheckpattern/domain/MessageBody.java | 43 +++++
.../domain/MessageHeader.java | 47 +++++
.../domain/MessageReference.java | 45 +++++
.../domain/UsageCostDetail.java | 43 +++++
.../claimcheckpattern/domain/UsageDetail.java | 44 +++++
.../UsageDetailPublisherFunction.java | 146 ++++++++++++++
.../utility/EventHandlerUtility.java | 66 +++++++
.../utility/MessageHandlerUtility.java | 127 ++++++++++++
.../HttpResponseMessageMock.java | 105 ++++++++++
.../UsageCostProcessorFunctionTest.java | 181 ++++++++++++++++++
.../UsageDetailPublisherFunctionTest.java | 119 ++++++++++++
.../utility/EventHandlerUtilityTest.java | 70 +++++++
.../utility/MessageHandlerUtilityTest.java | 114 +++++++++++
.../org.mockito.plugins.MockMaker | 1 +
.../subscriptionValidationEvent.json | 15 ++
.../src/test/resources/usageDetailEvent.json | 15 ++
.../etc/Claim-Check-Pattern.png | Bin 0 -> 53447 bytes
.../etc/claim-check-pattern.urm.puml | 117 +++++++++++
.../etc/class-diagram.png | Bin 0 -> 110789 bytes
cloud-claim-check-pattern/pom.xml | 69 +++++++
28 files changed, 2010 insertions(+)
create mode 100644 cloud-claim-check-pattern/.gitignore
create mode 100644 cloud-claim-check-pattern/README.md
create mode 100644 cloud-claim-check-pattern/call-usage-app/.gitignore
create mode 100644 cloud-claim-check-pattern/call-usage-app/etc/call-usage-app.urm.puml
create mode 100644 cloud-claim-check-pattern/call-usage-app/host.json
create mode 100644 cloud-claim-check-pattern/call-usage-app/pom.xml
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/consumer/callcostprocessor/functions/UsageCostProcessorFunction.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/Message.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageBody.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageHeader.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageReference.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/UsageCostDetail.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/UsageDetail.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/producer/calldetails/functions/UsageDetailPublisherFunction.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/utility/EventHandlerUtility.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/utility/MessageHandlerUtility.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/HttpResponseMessageMock.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/consumer/callcostprocessor/functions/UsageCostProcessorFunctionTest.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/producer/calldetails/functions/UsageDetailPublisherFunctionTest.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/utility/EventHandlerUtilityTest.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/utility/MessageHandlerUtilityTest.java
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/test/resources/subscriptionValidationEvent.json
create mode 100644 cloud-claim-check-pattern/call-usage-app/src/test/resources/usageDetailEvent.json
create mode 100644 cloud-claim-check-pattern/etc/Claim-Check-Pattern.png
create mode 100644 cloud-claim-check-pattern/etc/claim-check-pattern.urm.puml
create mode 100644 cloud-claim-check-pattern/etc/class-diagram.png
create mode 100644 cloud-claim-check-pattern/pom.xml
diff --git a/cloud-claim-check-pattern/.gitignore b/cloud-claim-check-pattern/.gitignore
new file mode 100644
index 000000000..f508cf94f
--- /dev/null
+++ b/cloud-claim-check-pattern/.gitignore
@@ -0,0 +1,38 @@
+# Build output
+target/
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# IDE
+.idea/
+*.iml
+.settings/
+.project
+.classpath
+
+# macOS
+.DS_Store
+
+# Azure Functions
+local.settings.json
+bin/
+obj/
diff --git a/cloud-claim-check-pattern/README.md b/cloud-claim-check-pattern/README.md
new file mode 100644
index 000000000..0259f5782
--- /dev/null
+++ b/cloud-claim-check-pattern/README.md
@@ -0,0 +1,82 @@
+---
+layout: pattern
+title: Claim Check Pattern
+folder: cloud-claim-check-pattern
+permalink: /patterns/cloud-claim-check-pattern/
+categories: Cloud
+tags:
+ - Cloud distributed
+ - Microservices
+---
+
+## Name
+
+[Claim Check Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/claim-check)
+
+## Also known as
+
+[Reference-Based Messaging](https://www.enterpriseintegrationpatterns.com/patterns/messaging/StoreInLibrary.html)
+
+## Intent
+
+- Reduce the load of data transfer through the Internet. Instead of sending actual data directly, just send the message reference.
+- Improve data security. As only message reference is shared, no data is exposed to the Internet.
+
+## Explanation
+
+Real-World Example
+
+> Suppose if you want to build a photo processing system. A photo processing system takes an image as input, processes it, and outputs a different set of images. Consider system has one persistent storage, one input component, ten processing components, messaging platform. Once a photo is given to the input component, it stores that image on persistent storage. It then creates ten different messages/events with the same image location and publishes them to the messaging platform. The messaging platform triggers ten different processing components. The ten processing components extract information about image location from the received event and then read an image from persistent storage. They generate ten different images from the original image and drop these images again to persistent storage.
+
+In Plain words
+
+> Split a large message into a claim check and a payload. Send the claim check to the messaging platform and store the payload to an external service. This pattern allows large messages to be processed while protecting the message bus and the client from being overwhelmed or slowed down. This pattern also helps to reduce costs, as storage is usually cheaper than resource units used by the messaging platform.([ref](https://docs.microsoft.com/en-us/azure/architecture/patterns/claim-check))
+
+## Architecture Diagram
+
+
+
+## Applicability
+
+Use the Claim Check Pattern when
+
+- Huge processing data causes a lot of bandwidth consumption to transfer data through the Internet.
+- To secure your data transfer by storing in common persistent storage.
+- Using a cloud platform - Azure Functions or AWS Lambda, Azure EventGrid or AWS Event Bridge, Azure Blob Storage or AWS S3 Bucket.
+- Each service must be independent and idempotent. Output data is dropped to persistent storage by the service.
+- Publish-subscribe messaging pattern needs to be used.
+
+## Consequences
+
+- This pattern is stateless. Any compute API will not store any data.
+- You must have persistent storage and a reliable messaging platform.
+
+## Tutorials
+
+### Workflow
+
+Suppose a telecom company wants to build call cost calculator system which generate the call cost daily. At the end of each day, details of the calls made by the consumers are stored somewhere. The call calculator system will read this data and generate call cost data for each user. Consumers will be billed using this generated data in case of postpaid service.
+
+Producer class( `UsageDetailPublisherFunction` Azure Function) will generate call usage details (here we are generating call data in producer class itself. In real world scenario, it will read from storage). `UsageDetailPublisherFunction` creates a message. Message consists of message header and message body. Message header is basically an event grid event or claim or message reference. Message body contains actual data. `UsageDetailPublisherFunction` sends a message header to Event Grid topic `usage-detail` and drops an entire message to the blob storage. Event Grid then sent this message header to the `UsageCostProcessorFunction` Azure function. It will read the entire message from blob storage with the help of the header, will calculate call cost and drop the result to the blob storage.
+
+### Class Diagrams
+
+
+
+### Setup
+
+- Any operating system can be used macOS, Windows, Linux as everything is deployed on Azure.
+- Install Java JDK 11 and set up Java environmental variables.
+- Install Git.
+- Install Visual Studio Code.
+- Install [ Azure Functions extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) to be able to deploy using Visual studio.
+
+### Storage Data
+
+The data is stored in the Azure blob storage in the container `callusageapp`. For every trigger, one GUID is created. Under the `GUID folder`, 2 files will be created `input.json` and `output.json`.
+`Input.json` is dropped `producer` azure function which contains call usage details.` Output.json` contains call cost details which are dropped by the `consumer` azure function.
+
+## Credits
+
+- [Messaging Pattern - Claim Check](https://www.enterpriseintegrationpatterns.com/patterns/messaging/StoreInLibrary.html)
+- [Azure Architecture Pattern - Claim Check Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/claim-check)
diff --git a/cloud-claim-check-pattern/call-usage-app/.gitignore b/cloud-claim-check-pattern/call-usage-app/.gitignore
new file mode 100644
index 000000000..f508cf94f
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/.gitignore
@@ -0,0 +1,38 @@
+# Build output
+target/
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# IDE
+.idea/
+*.iml
+.settings/
+.project
+.classpath
+
+# macOS
+.DS_Store
+
+# Azure Functions
+local.settings.json
+bin/
+obj/
diff --git a/cloud-claim-check-pattern/call-usage-app/etc/call-usage-app.urm.puml b/cloud-claim-check-pattern/call-usage-app/etc/call-usage-app.urm.puml
new file mode 100644
index 000000000..0ffec48b1
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/etc/call-usage-app.urm.puml
@@ -0,0 +1,117 @@
+@startuml
+package com.iluwatar.claimcheckpattern.producer.calldetails.functions {
+ class UsageDetailPublisherFunction {
+ - eventHandlerUtility : EventHandlerUtility
+ - messageHandlerUtility : MessageHandlerUtility
+ + UsageDetailPublisherFunction()
+ + UsageDetailPublisherFunction(messageHandlerUtility : MessageHandlerUtility, eventHandlerUtility : EventHandlerUtility)
+ + run(request : HttpRequestMessage>, context : ExecutionContext) : HttpResponseMessage
+ }
+}
+package com.iluwatar.claimcheckpattern.domain {
+ class Message {
+ - messageBody : MessageBody
+ - messageHeader : MessageHeader
+ + Message()
+ + getMessageBody() : MessageBody
+ + getMessageHeader() : MessageHeader
+ + setMessageBody(messageBody : MessageBody)
+ + setMessageHeader(messageHeader : MessageHeader)
+ }
+ class MessageBody {
+ - data : List
+ + MessageBody()
+ + getData() : List
+ + setData(data : List)
+ }
+ class MessageHeader {
+ - data : Object
+ - dataVersion : String
+ - eventTime : String
+ - eventType : String
+ - id : String
+ - subject : String
+ - topic : String
+ + MessageHeader()
+ + getData() : Object
+ + getDataVersion() : String
+ + getEventTime() : String
+ + getEventType() : String
+ + getId() : String
+ + getSubject() : String
+ + getTopic() : String
+ + setData(data : Object)
+ + setDataVersion(dataVersion : String)
+ + setEventTime(eventTime : String)
+ + setEventType(eventType : String)
+ + setId(id : String)
+ + setSubject(subject : String)
+ + setTopic(topic : String)
+ }
+ class MessageReference {
+ - dataFileName : String
+ - dataLocation : String
+ + MessageReference()
+ + MessageReference(dataLocation : String, dataFileName : String)
+ + getDataFileName() : String
+ + getDataLocation() : String
+ + setDataFileName(dataFileName : String)
+ + setDataLocation(dataLocation : String)
+ }
+ class UsageCostDetail {
+ - callCost : double
+ - dataCost : double
+ - userId : String
+ + UsageCostDetail()
+ + getCallCost() : double
+ + getDataCost() : double
+ + getUserId() : String
+ + setCallCost(callCost : double)
+ + setDataCost(dataCost : double)
+ + setUserId(userId : String)
+ }
+ class UsageDetail {
+ - data : int
+ - duration : int
+ - userId : String
+ + UsageDetail()
+ + getData() : int
+ + getDuration() : int
+ + getUserId() : String
+ + setData(data : int)
+ + setDuration(duration : int)
+ + setUserId(userId : String)
+ }
+}
+package com.iluwatar.claimcheckpattern.utility {
+ class EventHandlerUtility {
+ - customEventClient : EventGridPublisherClient
+ + EventHandlerUtility()
+ + EventHandlerUtility(customEventClient : EventGridPublisherClient)
+ + publishEvent(customEvent : T, logger : Logger)
+ }
+ class MessageHandlerUtility {
+ - blobServiceClient : BlobServiceClient
+ + MessageHandlerUtility()
+ + MessageHandlerUtility(blobServiceClient : BlobServiceClient)
+ + dropToPersistantStorage(message : Message, logger : Logger)
+ + readFromPersistantStorage(messageReference : MessageReference, logger : Logger) : Message
+ }
+}
+package com.iluwatar.claimcheckpattern.consumer.callcostprocessor.functions {
+ class UsageCostProcessorFunction {
+ - messageHandlerUtilityForUsageCostDetail : MessageHandlerUtility
+ - messageHandlerUtilityForUsageDetail : MessageHandlerUtility
+ + UsageCostProcessorFunction()
+ + UsageCostProcessorFunction(messageHandlerUtilityForUsageDetail : MessageHandlerUtility, messageHandlerUtilityForUsageCostDetail : MessageHandlerUtility)
+ - calculateUsageCostDetails(usageDetailsList : List) : List
+ + run(request : HttpRequestMessage>, context : ExecutionContext) : HttpResponseMessage
+ }
+}
+UsageCostProcessorFunction --> "-messageHandlerUtilityForUsageDetail" MessageHandlerUtility
+Message --> "-messageBody" MessageBody
+UsageDetailPublisherFunction --> "-eventHandlerUtility" EventHandlerUtility
+Builder ..+ HttpResponseMessage
+UsageDetailPublisherFunction --> "-messageHandlerUtility" MessageHandlerUtility
+Message --> "-messageHeader" MessageHeader
+@enduml
\ No newline at end of file
diff --git a/cloud-claim-check-pattern/call-usage-app/host.json b/cloud-claim-check-pattern/call-usage-app/host.json
new file mode 100644
index 000000000..4ac89572d
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/host.json
@@ -0,0 +1,7 @@
+{
+ "version": "2.0",
+ "extensionBundle": {
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
+ "version": "[1.*, 2.0.0)"
+ }
+}
\ No newline at end of file
diff --git a/cloud-claim-check-pattern/call-usage-app/pom.xml b/cloud-claim-check-pattern/call-usage-app/pom.xml
new file mode 100644
index 000000000..a1bc7fc10
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/pom.xml
@@ -0,0 +1,150 @@
+
+
+
+ 4.0.0
+
+
+
+ com.iluwatar
+ claim-check-pattern
+ 1.25.0-SNAPSHOT
+
+
+ call-usage-app
+ call-usage-app
+ jar
+
+
+ UTF-8
+ 1.14.0
+ 1.4.2
+ CallUsageApp
+
+
+
+
+
+ com.azure
+ azure-sdk-bom
+ 1.0.4
+ pom
+ import
+
+
+
+
+
+
+ com.microsoft.azure.functions
+ azure-functions-java-library
+ ${azure.functions.java.library.version}
+
+
+
+ com.azure
+ azure-messaging-eventgrid
+
+
+
+ com.azure
+ azure-storage-blob
+ 12.13.0
+
+
+
+ org.slf4j
+ slf4j-simple
+ test
+
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
+
+
+
+
+ com.microsoft.azure
+ azure-functions-maven-plugin
+ ${azure.functions.maven.plugin.version}
+
+
+ ${functionAppName}
+
+ java-functions-group
+
+ java-functions-app-service-plan
+
+
+ westus
+
+
+
+
+
+
+
+
+ windows
+ 11
+
+
+
+ FUNCTIONS_EXTENSION_VERSION
+ ~3
+
+
+
+
+
+ package-functions
+
+ package
+
+
+
+
+
+
+ maven-clean-plugin
+ 3.1.0
+
+
+
+ obj
+
+
+
+
+
+
+
diff --git a/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/consumer/callcostprocessor/functions/UsageCostProcessorFunction.java b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/consumer/callcostprocessor/functions/UsageCostProcessorFunction.java
new file mode 100644
index 000000000..652095727
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/consumer/callcostprocessor/functions/UsageCostProcessorFunction.java
@@ -0,0 +1,168 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.consumer.callcostprocessor.functions;
+
+import com.azure.core.util.BinaryData;
+import com.azure.core.util.serializer.TypeReference;
+import com.azure.messaging.eventgrid.EventGridEvent;
+import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationEventData;
+import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationResponse;
+import com.iluwatar.claimcheckpattern.domain.Message;
+import com.iluwatar.claimcheckpattern.domain.MessageBody;
+import com.iluwatar.claimcheckpattern.domain.MessageHeader;
+import com.iluwatar.claimcheckpattern.domain.MessageReference;
+import com.iluwatar.claimcheckpattern.domain.UsageCostDetail;
+import com.iluwatar.claimcheckpattern.domain.UsageDetail;
+import com.iluwatar.claimcheckpattern.utility.MessageHandlerUtility;
+import com.microsoft.azure.functions.ExecutionContext;
+import com.microsoft.azure.functions.HttpMethod;
+import com.microsoft.azure.functions.HttpRequestMessage;
+import com.microsoft.azure.functions.HttpResponseMessage;
+import com.microsoft.azure.functions.HttpStatus;
+import com.microsoft.azure.functions.annotation.AuthorizationLevel;
+import com.microsoft.azure.functions.annotation.FunctionName;
+import com.microsoft.azure.functions.annotation.HttpTrigger;
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Azure Functions with HTTP Trigger.
+ * This is Consumer class.
+ */
+public class UsageCostProcessorFunction {
+
+ private MessageHandlerUtility messageHandlerUtilityForUsageDetail;
+ private MessageHandlerUtility messageHandlerUtilityForUsageCostDetail;
+
+ public UsageCostProcessorFunction() {
+ this.messageHandlerUtilityForUsageDetail = new MessageHandlerUtility<>();
+ this.messageHandlerUtilityForUsageCostDetail = new MessageHandlerUtility<>();
+ }
+
+ public UsageCostProcessorFunction(
+ MessageHandlerUtility messageHandlerUtilityForUsageDetail,
+ MessageHandlerUtility messageHandlerUtilityForUsageCostDetail) {
+ this.messageHandlerUtilityForUsageDetail = messageHandlerUtilityForUsageDetail;
+ this.messageHandlerUtilityForUsageCostDetail = messageHandlerUtilityForUsageCostDetail;
+ }
+
+ /**
+ * Azure function which gets triggered when event grid event send event to it.
+ * After receiving event, it read input file from blob storage, calculate call cost details.
+ * It creates new message with cost details and drop message to blob storage.
+ * @param request represents HttpRequestMessage
+ * @param context represents ExecutionContext
+ * @return HttpResponseMessage
+ */
+ @FunctionName("UsageCostProcessorFunction")
+ public HttpResponseMessage run(@HttpTrigger(name = "req", methods = { HttpMethod.GET,
+ HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS)
+ HttpRequestMessage> request,
+ final ExecutionContext context) {
+ try {
+ var eventGridEvents = EventGridEvent.fromString(request.getBody().get());
+ for (var eventGridEvent : eventGridEvents) {
+ // Handle system events
+ if (eventGridEvent.getEventType()
+ .equals("Microsoft.EventGrid.SubscriptionValidationEvent")) {
+ SubscriptionValidationEventData subscriptionValidationEventData = eventGridEvent.getData()
+ .toObject(SubscriptionValidationEventData.class);
+ // Handle the subscription validation event
+ var responseData = new SubscriptionValidationResponse();
+ responseData.setValidationResponse(subscriptionValidationEventData.getValidationCode());
+ return request.createResponseBuilder(HttpStatus.OK).body(responseData).build();
+
+ } else if (eventGridEvent.getEventType().equals("UsageDetail")) {
+ // Get message header and reference
+ var messageReference = eventGridEvent.getData()
+ .toObject(MessageReference.class);
+
+ // Read message from persistent storage
+ var message = this.messageHandlerUtilityForUsageDetail
+ .readFromPersistantStorage(messageReference, context.getLogger());
+
+ // Get Data and generate cost details
+ List usageDetailsList = BinaryData.fromObject(
+ message.getMessageBody().getData())
+ .toObject(new TypeReference<>() {
+ });
+ var usageCostDetailsList = calculateUsageCostDetails(usageDetailsList);
+
+ // Create message body
+ var newMessageBody = new MessageBody();
+ newMessageBody.setData(usageCostDetailsList);
+
+ // Create message header
+ var newMessageReference = new MessageReference("callusageapp",
+ eventGridEvent.getId() + "/output.json");
+ var newMessageHeader = new MessageHeader();
+ newMessageHeader.setId(eventGridEvent.getId());
+ newMessageHeader.setSubject("UsageCostProcessor");
+ newMessageHeader.setTopic("");
+ newMessageHeader.setEventType("UsageCostDetail");
+ newMessageHeader.setEventTime(OffsetDateTime.now().toString());
+ newMessageHeader.setData(newMessageReference);
+ newMessageHeader.setDataVersion("v1.0");
+
+ // Create entire message
+ var newMessage = new Message();
+ newMessage.setMessageHeader(newMessageHeader);
+ newMessage.setMessageBody(newMessageBody);
+
+ // Drop data to persistent storage
+ this.messageHandlerUtilityForUsageCostDetail.dropToPersistantStorage(newMessage,
+ context.getLogger());
+
+ context.getLogger().info("Message is dropped successfully");
+ return request.createResponseBuilder(HttpStatus.OK)
+ .body("Message is dropped successfully").build();
+ }
+ }
+ } catch (Exception e) {
+ context.getLogger().warning(e.getMessage());
+ }
+
+ return request.createResponseBuilder(HttpStatus.INTERNAL_SERVER_ERROR).body(null).build();
+ }
+
+ private List calculateUsageCostDetails(List usageDetailsList) {
+ if (usageDetailsList == null) {
+ return null;
+ }
+ var usageCostDetailsList = new ArrayList();
+
+ usageDetailsList.forEach(usageDetail -> {
+ var usageCostDetail = new UsageCostDetail();
+ usageCostDetail.setUserId(usageDetail.getUserId());
+ usageCostDetail.setCallCost(usageDetail.getDuration() * 0.30); // 0.30₹ per minute
+ usageCostDetail.setDataCost(usageDetail.getData() * 0.20); // 0.20₹ per MB
+
+ usageCostDetailsList.add(usageCostDetail);
+ });
+
+ return usageCostDetailsList;
+ }
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/Message.java b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/Message.java
new file mode 100644
index 000000000..d85653dcd
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/Message.java
@@ -0,0 +1,43 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * It is the message which gets dropped or read by Producer or Consumer Azure functions.
+ * It is stored in the json format.
+ * @param represents UsageDetail or UsageCostDetail
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Message {
+ private MessageHeader messageHeader;
+
+ private MessageBody messageBody;
+
+}
\ No newline at end of file
diff --git a/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageBody.java b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageBody.java
new file mode 100644
index 000000000..e8284e04e
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageBody.java
@@ -0,0 +1,43 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.domain;
+
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * It is message body of the message.
+ * It stores actual data in our case UsageCostDetail or UsageDetail.
+ * @param represents UsageDetail or UsageCostDetail
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class MessageBody {
+
+ private List data;
+
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageHeader.java b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageHeader.java
new file mode 100644
index 000000000..71e897ca9
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageHeader.java
@@ -0,0 +1,47 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * This is message header or event which is sent to Event Grid.
+ * Its structure is same as Azure Event Grid Event Class.
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class MessageHeader {
+
+ private String id;
+ private String subject;
+ private String topic;
+ private String eventType;
+ private String eventTime;
+ private Object data;
+ private String dataVersion;
+
+}
\ No newline at end of file
diff --git a/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageReference.java b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageReference.java
new file mode 100644
index 000000000..9eab15a3a
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/MessageReference.java
@@ -0,0 +1,45 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * This is claim/message reference class.
+ * It contains the information about data where it is stored in persistent storage
+ * and file name.
+ * dataLocation is blob storage container name.
+ * dataFileName is file name in above container.
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class MessageReference {
+
+ private String dataLocation;
+ private String dataFileName;
+
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/UsageCostDetail.java b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/UsageCostDetail.java
new file mode 100644
index 000000000..8930ea3f5
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/UsageCostDetail.java
@@ -0,0 +1,43 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * This is call cost details class.
+ * It stores userId of the caller, call duration cost and data cost.
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class UsageCostDetail {
+
+ private String userId;
+ private double callCost;
+ private double dataCost;
+
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/UsageDetail.java b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/UsageDetail.java
new file mode 100644
index 000000000..c8f0395db
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/domain/UsageDetail.java
@@ -0,0 +1,44 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * This is call usage detail calls.
+ * It stores userId of the caller, call duration and data used.
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class UsageDetail {
+
+ private String userId;
+
+ private int duration;
+
+ private int data;
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/producer/calldetails/functions/UsageDetailPublisherFunction.java b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/producer/calldetails/functions/UsageDetailPublisherFunction.java
new file mode 100644
index 000000000..2f58f8f48
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/producer/calldetails/functions/UsageDetailPublisherFunction.java
@@ -0,0 +1,146 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.producer.calldetails.functions;
+
+import com.azure.messaging.eventgrid.EventGridEvent;
+import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationEventData;
+import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationResponse;
+import com.iluwatar.claimcheckpattern.domain.Message;
+import com.iluwatar.claimcheckpattern.domain.MessageBody;
+import com.iluwatar.claimcheckpattern.domain.MessageHeader;
+import com.iluwatar.claimcheckpattern.domain.MessageReference;
+import com.iluwatar.claimcheckpattern.domain.UsageDetail;
+import com.iluwatar.claimcheckpattern.utility.EventHandlerUtility;
+import com.iluwatar.claimcheckpattern.utility.MessageHandlerUtility;
+import com.microsoft.azure.functions.ExecutionContext;
+import com.microsoft.azure.functions.HttpMethod;
+import com.microsoft.azure.functions.HttpRequestMessage;
+import com.microsoft.azure.functions.HttpResponseMessage;
+import com.microsoft.azure.functions.HttpStatus;
+import com.microsoft.azure.functions.annotation.AuthorizationLevel;
+import com.microsoft.azure.functions.annotation.FunctionName;
+import com.microsoft.azure.functions.annotation.HttpTrigger;
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Random;
+import java.util.UUID;
+
+/**
+ * Azure Functions with HTTP Trigger.
+ * This is Producer class.
+ */
+public class UsageDetailPublisherFunction {
+
+ private MessageHandlerUtility messageHandlerUtility;
+ private EventHandlerUtility eventHandlerUtility;
+
+ public UsageDetailPublisherFunction() {
+ this.messageHandlerUtility = new MessageHandlerUtility<>();
+ this.eventHandlerUtility = new EventHandlerUtility<>();
+ }
+
+ public UsageDetailPublisherFunction(MessageHandlerUtility messageHandlerUtility,
+ EventHandlerUtility eventHandlerUtility) {
+ this.messageHandlerUtility = messageHandlerUtility;
+ this.eventHandlerUtility = eventHandlerUtility;
+ }
+
+ /**
+ * Azure function which create message, drop it in persistent storage
+ * and publish the event to Event Grid topic.
+ * @param request represents HttpRequestMessage
+ * @param context represents ExecutionContext
+ * @return HttpResponseMessage
+ */
+ @FunctionName("UsageDetailPublisherFunction")
+ public HttpResponseMessage run(@HttpTrigger(name = "req", methods = {
+ HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS)
+ HttpRequestMessage> request,
+ final ExecutionContext context) {
+ try {
+
+ var eventGridEvents = EventGridEvent.fromString(request.getBody().get());
+
+ for (EventGridEvent eventGridEvent : eventGridEvents) {
+ // Handle system events
+ if (eventGridEvent.getEventType()
+ .equals("Microsoft.EventGrid.SubscriptionValidationEvent")) {
+ SubscriptionValidationEventData subscriptionValidationEventData = eventGridEvent.getData()
+ .toObject(SubscriptionValidationEventData.class);
+ // Handle the subscription validation event
+ var responseData = new SubscriptionValidationResponse();
+ responseData.setValidationResponse(subscriptionValidationEventData.getValidationCode());
+ return request.createResponseBuilder(HttpStatus.OK).body(responseData).build();
+
+ } else if (eventGridEvent.getEventType().equals("UsageDetail")) {
+ // Create message body
+ var messageBody = new MessageBody();
+ var usageDetailsList = new ArrayList();
+ var random = new Random();
+ for (int i = 0; i < 51; i++) {
+ var usageDetail = new UsageDetail();
+ usageDetail.setUserId("userId" + i);
+ usageDetail.setData(random.nextInt(500));
+ usageDetail.setDuration(random.nextInt(500));
+
+ usageDetailsList.add(usageDetail);
+ }
+ messageBody.setData(usageDetailsList);
+
+ // Create message header
+ var messageHeader = new MessageHeader();
+ messageHeader.setId(UUID.randomUUID().toString());
+ messageHeader.setSubject("UsageDetailPublisher");
+ messageHeader.setTopic("usagecostprocessorfunction-topic");
+ messageHeader.setEventType("UsageDetail");
+ messageHeader.setEventTime(OffsetDateTime.now().toString());
+ var messageReference = new MessageReference("callusageapp",
+ messageHeader.getId() + "/input.json");
+ messageHeader.setData(messageReference);
+ messageHeader.setDataVersion("v1.0");
+
+ // Create entire message
+ var message = new Message();
+ message.setMessageHeader(messageHeader);
+ message.setMessageBody(messageBody);
+
+ // Drop data to persistent storage
+ this.messageHandlerUtility.dropToPersistantStorage(message, context.getLogger());
+
+ // Publish event to event grid topic
+ eventHandlerUtility.publishEvent(messageHeader, context.getLogger());
+
+ context.getLogger().info("Message is dropped and event is published successfully");
+ return request.createResponseBuilder(HttpStatus.OK).body(message).build();
+ }
+ }
+ } catch (Exception e) {
+ context.getLogger().warning(e.getMessage());
+ }
+
+ return request.createResponseBuilder(HttpStatus.OK).body(null).build();
+ }
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/utility/EventHandlerUtility.java b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/utility/EventHandlerUtility.java
new file mode 100644
index 000000000..c037db10d
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/utility/EventHandlerUtility.java
@@ -0,0 +1,66 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.utility;
+
+import com.azure.core.credential.AzureKeyCredential;
+import com.azure.core.util.BinaryData;
+import com.azure.messaging.eventgrid.EventGridPublisherClient;
+import com.azure.messaging.eventgrid.EventGridPublisherClientBuilder;
+import java.util.logging.Logger;
+
+/**
+ * This class is event publisher utility which published message header to Event Grid topic.
+ * @param represents UsageDetail or UsageCostDetail
+ */
+public class EventHandlerUtility {
+
+ private EventGridPublisherClient customEventClient;
+
+ /** Default constructor.
+ */
+ public EventHandlerUtility() {
+ this.customEventClient = new EventGridPublisherClientBuilder()
+ .endpoint(System.getenv("EventGridURL"))
+ .credential(new AzureKeyCredential(System.getenv("EventGridKey")))
+ .buildCustomEventPublisherClient();
+ }
+
+ /**
+ Parameterized constructor.
+ */
+ public EventHandlerUtility(EventGridPublisherClient customEventClient) {
+ this.customEventClient = customEventClient;
+ }
+
+ /**
+ Method for publishing event to Event Grid Topic.
+ */
+ public void publishEvent(T customEvent, Logger logger) {
+ try {
+ customEventClient.sendEvent(BinaryData.fromObject(customEvent));
+ } catch (Exception e) {
+ logger.info(e.getMessage());
+ }
+ }
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/utility/MessageHandlerUtility.java b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/utility/MessageHandlerUtility.java
new file mode 100644
index 000000000..40aac8ab9
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/main/java/com/iluwatar/claimcheckpattern/utility/MessageHandlerUtility.java
@@ -0,0 +1,127 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.utility;
+
+import com.azure.core.util.BinaryData;
+import com.azure.core.util.serializer.TypeReference;
+import com.azure.storage.blob.BlobClient;
+import com.azure.storage.blob.BlobContainerClient;
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.iluwatar.claimcheckpattern.domain.Message;
+import com.iluwatar.claimcheckpattern.domain.MessageReference;
+import java.util.logging.Logger;
+
+/**
+ * This class read and drop message from Azure blob storage.
+ * @param represents UsageDetail or UsageCostDetail
+ */
+public class MessageHandlerUtility {
+
+ private BlobServiceClient blobServiceClient;
+
+ /**
+ * Parameterized constructor.
+ * @param blobServiceClient represents BlobServiceClient
+ */
+ public MessageHandlerUtility(BlobServiceClient blobServiceClient) {
+ this.blobServiceClient = blobServiceClient;
+ }
+
+ /**
+ * Default constructor.
+ */
+ public MessageHandlerUtility() {
+ // Create a BlobServiceClient object which will be used to create a container
+ // client
+ this.blobServiceClient = new BlobServiceClientBuilder()
+ .connectionString(System.getenv("BlobStorageConnectionString")).buildClient();
+
+ }
+
+ /**
+ * Read message from blob storage.
+ * @param messageReference represents MessageReference
+ * @param logger represents Logger
+ * @return Message
+ */
+ public Message readFromPersistantStorage(MessageReference messageReference, Logger logger) {
+ Message message = null;
+ try {
+
+ // Get container name from message reference
+ String containerName = messageReference.getDataLocation();
+
+ // Get blob name from message reference
+ String blobName = messageReference.getDataFileName();
+
+ // Get container client
+ BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName);
+
+ // Get a reference to a blob
+ BlobClient blobClient = containerClient.getBlobClient(blobName);
+
+ // download the blob
+ message = blobClient.downloadContent().toObject(new TypeReference>() {
+ });
+ } catch (Exception e) {
+ logger.info(e.getMessage());
+ }
+ return message;
+
+ }
+
+ /**
+ * Drop message to blob storage.
+ * @param message represents Message
+ * @param logger represents Logger
+ */
+ public void dropToPersistantStorage(Message message, Logger logger) {
+ try {
+
+ // Get message reference
+ MessageReference messageReference = (MessageReference) message.getMessageHeader().getData();
+
+ // Create a unique name for the container
+ String containerName = messageReference.getDataLocation();
+
+ // Create the container and return a container client object
+ BlobContainerClient containerClient = this.blobServiceClient
+ .getBlobContainerClient(containerName);
+ if (!containerClient.exists()) {
+ containerClient.create();
+ }
+
+ // Get a reference to a blob
+ BlobClient blobClient = containerClient.getBlobClient(messageReference.getDataFileName());
+
+ // Upload the blob
+ blobClient.upload(BinaryData.fromObject(message));
+ } catch (Exception e) {
+ logger.info(e.getMessage());
+ }
+
+ }
+
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/HttpResponseMessageMock.java b/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/HttpResponseMessageMock.java
new file mode 100644
index 000000000..94cb48240
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/HttpResponseMessageMock.java
@@ -0,0 +1,105 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern;
+
+import com.microsoft.azure.functions.HttpResponseMessage;
+import com.microsoft.azure.functions.HttpStatus;
+import com.microsoft.azure.functions.HttpStatusType;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The mock for HttpResponseMessage, can be used in unit tests to verify if the
+ * returned response by HTTP trigger function is correct or not.
+ */
+public class HttpResponseMessageMock implements HttpResponseMessage {
+ private int httpStatusCode;
+ private HttpStatusType httpStatus;
+ private Object body;
+ private Map headers;
+
+ public HttpResponseMessageMock(HttpStatusType status, Map headers, Object body) {
+ this.httpStatus = status;
+ this.httpStatusCode = status.value();
+ this.headers = headers;
+ this.body = body;
+ }
+
+ @Override
+ public HttpStatusType getStatus() {
+ return this.httpStatus;
+ }
+
+ @Override
+ public int getStatusCode() {
+ return httpStatusCode;
+ }
+
+ @Override
+ public String getHeader(String key) {
+ return this.headers.get(key);
+ }
+
+ @Override
+ public Object getBody() {
+ return this.body;
+ }
+
+ public static class HttpResponseMessageBuilderMock implements HttpResponseMessage.Builder {
+ private Object body;
+ private int httpStatusCode;
+ private Map headers = new HashMap<>();
+ private HttpStatusType httpStatus;
+
+ public Builder status(HttpStatus status) {
+ this.httpStatusCode = status.value();
+ this.httpStatus = status;
+ return this;
+ }
+
+ @Override
+ public Builder status(HttpStatusType httpStatusType) {
+ this.httpStatusCode = httpStatusType.value();
+ this.httpStatus = httpStatusType;
+ return this;
+ }
+
+ @Override
+ public HttpResponseMessage.Builder header(String key, String value) {
+ this.headers.put(key, value);
+ return this;
+ }
+
+ @Override
+ public HttpResponseMessage.Builder body(Object body) {
+ this.body = body;
+ return this;
+ }
+
+ @Override
+ public HttpResponseMessage build() {
+ return new HttpResponseMessageMock(this.httpStatus, this.headers, this.body);
+ }
+ }
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/consumer/callcostprocessor/functions/UsageCostProcessorFunctionTest.java b/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/consumer/callcostprocessor/functions/UsageCostProcessorFunctionTest.java
new file mode 100644
index 000000000..940a39af7
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/consumer/callcostprocessor/functions/UsageCostProcessorFunctionTest.java
@@ -0,0 +1,181 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.consumer.callcostprocessor.functions;
+
+import com.iluwatar.claimcheckpattern.HttpResponseMessageMock;
+import com.iluwatar.claimcheckpattern.domain.Message;
+import com.iluwatar.claimcheckpattern.domain.MessageBody;
+import com.iluwatar.claimcheckpattern.domain.MessageHeader;
+import com.iluwatar.claimcheckpattern.domain.MessageReference;
+import com.iluwatar.claimcheckpattern.domain.UsageCostDetail;
+import com.iluwatar.claimcheckpattern.domain.UsageDetail;
+import com.iluwatar.claimcheckpattern.utility.MessageHandlerUtility;
+import com.microsoft.azure.functions.ExecutionContext;
+import com.microsoft.azure.functions.HttpRequestMessage;
+import com.microsoft.azure.functions.HttpResponseMessage;
+import com.microsoft.azure.functions.HttpStatus;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.stubbing.Answer;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.OffsetDateTime;
+import java.util.*;
+import java.util.logging.Logger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit test for Function class.
+ */
+@ExtendWith(MockitoExtension.class)
+public class UsageCostProcessorFunctionTest {
+
+ @Mock
+ MessageHandlerUtility mockMessageHandlerUtilityForUsageADetail;
+ @Mock
+ MessageHandlerUtility mockMessageHandlerUtilityForUsageCostDetail;
+ @Mock
+ ExecutionContext context;
+
+ Message messageToDrop;
+ Message messageToRead;
+ MessageReference messageReference;
+ @InjectMocks
+ UsageCostProcessorFunction usageCostProcessorFunction;
+
+ @BeforeEach
+ public void setUp() {
+ var messageBodyUsageDetail = new MessageBody();
+ var usageDetailsList = new ArrayList();
+
+ var messageBodyUsageCostDetail = new MessageBody();
+ var usageCostDetailsList = new ArrayList();
+ for (int i = 0; i < 2; i++) {
+ var usageDetail = new UsageDetail();
+ usageDetail.setUserId("userId" + i);
+ usageDetail.setData(i + 1);
+ usageDetail.setDuration(i + 1);
+ usageDetailsList.add(usageDetail);
+
+ var usageCostDetail = new UsageCostDetail();
+ usageCostDetail.setUserId(usageDetail.getUserId());
+ usageCostDetail.setDataCost(usageDetail.getData() * 0.20);
+ usageCostDetail.setCallCost(usageDetail.getDuration() * 0.30);
+ usageCostDetailsList.add(usageCostDetail);
+ }
+ messageBodyUsageDetail.setData(usageDetailsList);
+ messageBodyUsageCostDetail.setData(usageCostDetailsList);
+
+ // Create message header
+ var messageHeader = new MessageHeader();
+ messageHeader.setId(UUID.randomUUID().toString());
+ messageHeader.setSubject("UsageDetailPublisher");
+ messageHeader.setTopic("usagecostprocessorfunction-topic");
+ messageHeader.setEventType("UsageDetail");
+ messageHeader.setEventTime(OffsetDateTime.now().toString());
+ this.messageReference = new MessageReference("callusageapp", "d8284456-dfff-4bd4-9cef-ea99f70f4835/input.json");
+ messageHeader.setData(messageReference);
+ messageHeader.setDataVersion("v1.0");
+
+ // Create entire message
+ messageToRead = new Message<>();
+ messageToRead.setMessageHeader(messageHeader);
+ messageToRead.setMessageBody(messageBodyUsageDetail);
+
+ messageToDrop = new Message<>();
+ messageToDrop.setMessageHeader(messageHeader);
+ messageToDrop.setMessageBody(messageBodyUsageCostDetail);
+
+ }
+
+ /**
+ * Unit test for HttpTriggerJava method.
+ */
+ @Test
+ public void shouldTriggerHttpAzureFunctionJavaWithSubscriptionValidationEventType() throws Exception {
+
+ // Setup
+ @SuppressWarnings("unchecked")
+ final HttpRequestMessage> req = mock(HttpRequestMessage.class);
+ String fileAbsolutePath = getClass().getResource("/subscriptionValidationEvent.json").getPath()
+ .replaceAll("%20", " "), jsonBody = Files.readString(Paths.get(fileAbsolutePath)).replaceAll("\n", " ");
+ doReturn(Optional.of(jsonBody)).when(req).getBody();
+ doAnswer(new Answer() {
+ @Override
+ public HttpResponseMessage.Builder answer(InvocationOnMock invocation) {
+ HttpStatus status = (HttpStatus) invocation.getArguments()[0];
+ return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status);
+ }
+ }).when(req).createResponseBuilder(any(HttpStatus.class));
+
+ final ExecutionContext context = mock(ExecutionContext.class);
+
+ // Invoke
+ final HttpResponseMessage ret = this.usageCostProcessorFunction.run(req, context);
+
+ // Verify
+ assertEquals(ret.getStatus(), HttpStatus.OK);
+ }
+
+ @Test
+ public void shouldTriggerHttpAzureFunctionJavaWithUsageDetailEventType() throws Exception {
+ // Setup
+ @SuppressWarnings("unchecked")
+ final HttpRequestMessage> req = mock(HttpRequestMessage.class);
+ String fileAbsolutePath = getClass().getResource("/usageDetailEvent.json").getPath().replaceAll("%20", " "),
+ jsonBody = Files.readString(Paths.get(fileAbsolutePath)).replaceAll("\n", " ");
+ doReturn(Optional.of(jsonBody)).when(req).getBody();
+ doReturn(Logger.getGlobal()).when(context).getLogger();
+
+ when(this.mockMessageHandlerUtilityForUsageADetail.readFromPersistantStorage(any(MessageReference.class),
+ eq(Logger.getGlobal()))).thenReturn(messageToRead);
+ doAnswer(new Answer() {
+ @Override
+ public HttpResponseMessage.Builder answer(InvocationOnMock invocation) {
+ HttpStatus status = (HttpStatus) invocation.getArguments()[0];
+ return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status);
+ }
+ }).when(req).createResponseBuilder(any(HttpStatus.class));
+
+ assertNotNull(this.mockMessageHandlerUtilityForUsageADetail);
+ assertEquals(this.messageToRead, this.mockMessageHandlerUtilityForUsageADetail
+ .readFromPersistantStorage(this.messageReference, Logger.getGlobal()));
+
+ // Invoke
+ final HttpResponseMessage ret = this.usageCostProcessorFunction.run(req, context);
+
+ // Verify
+ assertEquals(HttpStatus.OK, ret.getStatus());
+ assertEquals("Message is dropped successfully", ret.getBody());
+ }
+
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/producer/calldetails/functions/UsageDetailPublisherFunctionTest.java b/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/producer/calldetails/functions/UsageDetailPublisherFunctionTest.java
new file mode 100644
index 000000000..60083a81b
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/producer/calldetails/functions/UsageDetailPublisherFunctionTest.java
@@ -0,0 +1,119 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.producer.calldetails.functions;
+
+import com.iluwatar.claimcheckpattern.HttpResponseMessageMock;
+import com.iluwatar.claimcheckpattern.domain.MessageHeader;
+import com.iluwatar.claimcheckpattern.domain.UsageDetail;
+import com.iluwatar.claimcheckpattern.utility.EventHandlerUtility;
+import com.iluwatar.claimcheckpattern.utility.MessageHandlerUtility;
+import com.microsoft.azure.functions.ExecutionContext;
+import com.microsoft.azure.functions.HttpRequestMessage;
+import com.microsoft.azure.functions.HttpResponseMessage;
+import com.microsoft.azure.functions.HttpStatus;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.stubbing.Answer;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.logging.Logger;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit test for Function class.
+ */
+@ExtendWith(MockitoExtension.class)
+public class UsageDetailPublisherFunctionTest {
+ @Mock
+ MessageHandlerUtility mockMessageHandlerUtility;
+ @Mock
+ EventHandlerUtility mockEventHandlerUtility;
+
+ @InjectMocks
+ UsageDetailPublisherFunction usageDetailPublisherFunction;
+
+ /**
+ * Unit test for HttpTriggerJava method.
+ */
+ @Test
+ public void shouldTriggerHttpAzureFunctionJavaWithSubscriptionValidationEventType() throws Exception {
+
+ // Setup
+ @SuppressWarnings("unchecked")
+ final HttpRequestMessage> req = mock(HttpRequestMessage.class);
+ String fileAbsolutePath = getClass().getResource("/subscriptionValidationEvent.json").getPath()
+ .replaceAll("%20", " "), jsonBody = Files.readString(Paths.get(fileAbsolutePath)).replaceAll("\n", " ");
+ doReturn(Optional.of(jsonBody)).when(req).getBody();
+ doAnswer(new Answer() {
+ @Override
+ public HttpResponseMessage.Builder answer(InvocationOnMock invocation) {
+ HttpStatus status = (HttpStatus) invocation.getArguments()[0];
+ return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status);
+ }
+ }).when(req).createResponseBuilder(any(HttpStatus.class));
+
+ final ExecutionContext context = mock(ExecutionContext.class);
+
+ // Invoke
+ final HttpResponseMessage ret = this.usageDetailPublisherFunction.run(req, context);
+
+ // Verify
+ assertEquals(ret.getStatus(), HttpStatus.OK);
+ }
+
+ @Test
+ public void shouldTriggerHttpAzureFunctionJavaWithUsageDetailEventType() throws Exception {
+
+ // Setup
+ @SuppressWarnings("unchecked")
+ final HttpRequestMessage> req = mock(HttpRequestMessage.class);
+ String fileAbsolutePath = getClass().getResource("/usageDetailEvent.json").getPath().replaceAll("%20", " "),
+ jsonBody = Files.readString(Paths.get(fileAbsolutePath)).replaceAll("\n", " ");
+ doReturn(Optional.of(jsonBody)).when(req).getBody();
+ doAnswer(new Answer() {
+ @Override
+ public HttpResponseMessage.Builder answer(InvocationOnMock invocation) {
+ HttpStatus status = (HttpStatus) invocation.getArguments()[0];
+ return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status);
+ }
+ }).when(req).createResponseBuilder(any(HttpStatus.class));
+
+ final ExecutionContext context = mock(ExecutionContext.class);
+ doReturn(Logger.getGlobal()).when(context).getLogger();
+
+ // Invoke
+ final HttpResponseMessage ret = this.usageDetailPublisherFunction.run(req, context);
+
+ // Verify
+ assertEquals(ret.getStatus(), HttpStatus.OK);
+ }
+
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/utility/EventHandlerUtilityTest.java b/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/utility/EventHandlerUtilityTest.java
new file mode 100644
index 000000000..f16f60f6d
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/utility/EventHandlerUtilityTest.java
@@ -0,0 +1,70 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.utility;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import com.azure.core.util.BinaryData;
+import com.azure.messaging.eventgrid.EventGridPublisherClient;
+import com.iluwatar.claimcheckpattern.domain.Message;
+import com.iluwatar.claimcheckpattern.domain.UsageDetail;
+import java.util.logging.Logger;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class EventHandlerUtilityTest {
+
+ @Mock
+ EventGridPublisherClient mockCustomEventClient;
+
+ @InjectMocks
+ EventHandlerUtility> eventHandlerUtility;
+
+ @BeforeEach
+ public void setUp() {
+
+ System.setProperty("EventGridURL", "https://www.dummyEndpoint.com/api/events");
+ System.setProperty("EventGridKey", "EventGridURL");
+ }
+
+ @Test
+ void shouldPublishEvent() {
+ doNothing().when(mockCustomEventClient).sendEvent(any(BinaryData.class));
+ eventHandlerUtility.publishEvent(null, Logger.getLogger("logger"));
+ verify(mockCustomEventClient, times(1)).sendEvent(any(BinaryData.class));
+
+ }
+
+ @Test
+ void shouldPublishEventWithNullLogger() {
+ eventHandlerUtility.publishEvent(null, null);
+ verify(mockCustomEventClient, times(1)).sendEvent(any(BinaryData.class));
+ }
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/utility/MessageHandlerUtilityTest.java b/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/utility/MessageHandlerUtilityTest.java
new file mode 100644
index 000000000..4faab105d
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/test/java/com/iluwatar/claimcheckpattern/utility/MessageHandlerUtilityTest.java
@@ -0,0 +1,114 @@
+/*
+ * The MIT License
+ * Copyright © 2014-2021 Ilkka Seppälä
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.iluwatar.claimcheckpattern.utility;
+
+import com.azure.storage.blob.BlobClient;
+import com.azure.storage.blob.BlobContainerClient;
+import com.azure.storage.blob.BlobServiceClient;
+import com.iluwatar.claimcheckpattern.domain.*;
+import com.iluwatar.claimcheckpattern.utility.MessageHandlerUtility;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+import java.util.logging.Logger;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class MessageHandlerUtilityTest {
+ @Mock
+ private BlobClient mockBlobClient;
+
+ @Mock
+ private BlobContainerClient mockContainerClient;
+
+ @Mock
+ private BlobServiceClient mockBlobServiceClient;
+
+ @InjectMocks
+ private MessageHandlerUtility messageHandlerUtility;
+
+ private Message messageToPublish;
+ private MessageReference messageReference;
+
+ @BeforeEach
+ public void setUp() {
+ System.setProperty("BlobStorageConnectionString", "https://www.dummyEndpoint.com/api/blobs");
+
+ var messageBody = new MessageBody();
+ var usageDetailsList = new ArrayList();
+ var random = new Random();
+ for (int i = 0; i < 51; i++) {
+ var usageDetail = new UsageDetail();
+ usageDetail.setUserId("userId" + i);
+ usageDetail.setData(random.nextInt(500));
+ usageDetail.setDuration(random.nextInt(500));
+
+ usageDetailsList.add(usageDetail);
+ }
+ messageBody.setData(usageDetailsList);
+
+ // Create message header
+ var messageHeader = new MessageHeader();
+ messageHeader.setId(UUID.randomUUID().toString());
+ messageHeader.setSubject("UsageDetailPublisher");
+ messageHeader.setTopic("usagecostprocessorfunction-topic");
+ messageHeader.setEventType("UsageDetail");
+ messageHeader.setEventTime(OffsetDateTime.now().toString());
+ this.messageReference = new MessageReference("callusageapp", messageHeader.getId() + "/input.json");
+ messageHeader.setData(messageReference);
+ messageHeader.setDataVersion("v1.0");
+
+ // Create entire message
+ this.messageToPublish = new Message<>();
+ this.messageToPublish.setMessageHeader(messageHeader);
+ this.messageToPublish.setMessageBody(messageBody);
+
+ when(mockContainerClient.getBlobClient(anyString())).thenReturn(mockBlobClient);
+ when(mockBlobServiceClient.getBlobContainerClient(anyString())).thenReturn(mockContainerClient);
+ }
+
+ @Test
+ void shouldDropMessageToPersistantStorage() {
+ messageHandlerUtility.dropToPersistantStorage(messageToPublish, Logger.getLogger("logger"));
+ verify(mockBlobServiceClient, times(1)).getBlobContainerClient(anyString());
+ // verify(mockContainerClient, times(0)).exists();
+ }
+
+ @Test
+ void shouldReadMessageFromPersistantStorage() {
+
+ messageHandlerUtility.readFromPersistantStorage(messageReference, Logger.getLogger("logger"));
+ verify(mockBlobServiceClient, times(1)).getBlobContainerClient(anyString());
+ }
+}
diff --git a/cloud-claim-check-pattern/call-usage-app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/cloud-claim-check-pattern/call-usage-app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 000000000..ca6ee9cea
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file
diff --git a/cloud-claim-check-pattern/call-usage-app/src/test/resources/subscriptionValidationEvent.json b/cloud-claim-check-pattern/call-usage-app/src/test/resources/subscriptionValidationEvent.json
new file mode 100644
index 000000000..52bd6ee6a
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/test/resources/subscriptionValidationEvent.json
@@ -0,0 +1,15 @@
+[
+ {
+ "data": {
+ "validationCode": "C12F266E-79D9-4C0A-9922-5EF6201A34C2",
+ "validationUrl": "https://rp-centralindia.eventgrid.azure.net:553/eventsubscriptions/usagedetailpublisherfunction-subscription/validate?idu003dC12F266E-79D9-4C0A-9922-5EF6201A34C2u0026tu003d2021-10-26T08:10:52.4999377Zu0026apiVersionu003d2020-10-15-previewu0026tokenu003d30kEVoL8rAOWzQv0buurhrKnbP%2bGMtHObbA%2bax6wb4Y%3d"
+ },
+ "dataVersion": "2",
+ "eventTime": "2021-10-26T08:10:52.4999377Z",
+ "eventType": "Microsoft.EventGrid.SubscriptionValidationEvent",
+ "id": "e2a8466b-3dc0-46b7-bb7d-b999e51a2848",
+ "metadataVersion": "1",
+ "subject": "",
+ "topic": "/subscriptions/0fef643d-a6b1-48f9-a256-53fbd0d22f48/resourceGroups/resource-group-ccp/providers/Microsoft.EventGrid/domains/event-grid-domains-ccp/topics/usagedetailpublisherfunction-topic"
+ }
+]
diff --git a/cloud-claim-check-pattern/call-usage-app/src/test/resources/usageDetailEvent.json b/cloud-claim-check-pattern/call-usage-app/src/test/resources/usageDetailEvent.json
new file mode 100644
index 000000000..137649e29
--- /dev/null
+++ b/cloud-claim-check-pattern/call-usage-app/src/test/resources/usageDetailEvent.json
@@ -0,0 +1,15 @@
+[
+ {
+ "data": {
+ "dataFileName": "d8284456-dfff-4bd4-9cef-ea99f70f4835/input.json",
+ "dataLocation": "callusageapp"
+ },
+ "dataVersion": "v1.0",
+ "eventTime": "2021-10-25T19:17:15.7468501Z",
+ "eventType": "UsageDetail",
+ "id": "d8284456-dfff-4bd4-9cef-ea99f70f4835",
+ "metadataVersion": "1",
+ "subject": "UsageDetailPublisher",
+ "topic": "/subscriptions/0fef643d-a6b1-48f9-a256-ea99f70f4835/resourceGroups/resource-group-ccp/providers/Microsoft.EventGrid/domains/event-grid-domains-ccp/topics/usagecostprocessorfunction-topic"
+ }
+]
diff --git a/cloud-claim-check-pattern/etc/Claim-Check-Pattern.png b/cloud-claim-check-pattern/etc/Claim-Check-Pattern.png
new file mode 100644
index 0000000000000000000000000000000000000000..a2d8040afaf6c79495f50128bf17a0272cfe7370
GIT binary patch
literal 53447
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