diff --git a/CallAutomation_AppointmentBooking/pom.xml b/CallAutomation_AppointmentBooking/pom.xml
new file mode 100644
index 0000000..fe5038f
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/pom.xml
@@ -0,0 +1,180 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.0.6
+
+
+
+ com.communication.callautomation
+ AppointmentBooking
+ 1.0-SNAPSHOT
+
+ AppointmentBooking
+ CallAutomation Sample application for instructional usage
+
+
+ 18
+ 18
+ UTF-8
+ 1.18.26
+
+
+
+
+ test-feed
+ https://pkgs.dev.azure.com/msazuredev/6bd2c509-7068-4110-a280-4902014c36cb/_packaging/test-feed/maven/v1
+
+ true
+
+
+ true
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ com.vaadin.external.google
+ android-json
+
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ com.azure
+ azure-core
+ 1.39.0
+
+
+ com.azure
+ azure-identity
+ 1.9.0
+
+
+ com.azure
+ azure-communication-identity
+ 1.4.5
+
+
+ com.azure
+ azure-communication-common
+
+
+
+
+ com.azure
+ azure-communication-callautomation
+ 1.0.0-alpha.20230517.2
+
+
+ com.azure
+ azure-messaging-eventgrid
+ 4.15.1
+
+
+ com.azure
+ azure-communication-common
+ 2.0.0-beta.1
+
+
+ com.google.code.gson
+ gson
+ 2.10
+
+
+ org.projectlombok
+ lombok
+ provided
+ ${lombok.version}
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure-processor
+ true
+
+
+ org.json
+ json
+ 20230227
+
+
+
+
+
+
+
+ maven-clean-plugin
+ 3.2.0
+
+
+ maven-resources-plugin
+ 3.3.1
+
+
+ maven-compiler-plugin
+ 3.11.0
+
+
+ maven-surefire-plugin
+ 3.1.0
+
+
+ maven-jar-plugin
+ 3.3.0
+
+
+ maven-deploy-plugin
+ 3.1.1
+
+
+ maven-site-plugin
+ 3.12.1
+
+
+ maven-project-info-reports-plugin
+ 3.4.3
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+
+
+ java
+
+
+
+
+ com.communication.callautomation.Main
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/Main.java b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/Main.java
new file mode 100644
index 0000000..615a8e1
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/Main.java
@@ -0,0 +1,21 @@
+package com.communication.callautomation;
+
+import com.azure.core.http.HttpClient;
+import com.communication.callautomation.config.AcsConfig;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+@EnableConfigurationProperties(value = AcsConfig.class)
+public class Main {
+
+ @Bean
+ HttpClient azureHttpClient() {
+ return HttpClient.createDefault();
+ }
+ public static void main(String[] args) {
+ SpringApplication.run(Main.class, args);
+ }
+}
\ No newline at end of file
diff --git a/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/acs/client/AcsClient.java b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/acs/client/AcsClient.java
new file mode 100644
index 0000000..616715e
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/acs/client/AcsClient.java
@@ -0,0 +1,128 @@
+package com.communication.callautomation.acs.client;
+
+import com.azure.communication.callautomation.models.*;
+import com.azure.communication.common.CommunicationIdentifier;
+import com.azure.core.http.rest.Response;
+import com.azure.core.util.Context;
+import com.communication.callautomation.config.AcsConfig;
+import com.communication.callautomation.core.CallAutomationService;
+import com.communication.callautomation.core.model.EventInfo;
+import com.communication.callautomation.exceptions.AzureCallAutomationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+@Component
+@Slf4j
+public class AcsClient implements CallAutomationService {
+ private final AcsConfig acsConfig;
+ private final CallAutomationClientFactory callAutomationClientFactory;
+
+ @Autowired
+ public AcsClient(final AcsConfig acsConfig,
+ final CallAutomationClientFactory callAutomationClientFactory) {
+ this.acsConfig = acsConfig;
+ this.callAutomationClientFactory = callAutomationClientFactory;
+ }
+
+ @Override
+ public String answerCall(final EventInfo eventInfo) {
+ String incomingCallContext = eventInfo.getIncomingCallContext();
+ String callbackUri = acsConfig.getCallbackUri(eventInfo.getFromId());
+ String correlationId = eventInfo.getCorrelationId();
+
+ log.info("Answering media call with following callbackuri: {}", callbackUri);
+
+ try{
+ AnswerCallResult answerCallResponse = callAutomationClientFactory.getCallAutomationClient(correlationId)
+ .answerCall(incomingCallContext, callbackUri);
+ return answerCallResponse.getCallConnectionProperties().getCallConnectionId();
+ } catch(Exception e) {
+ log.error("Error occurred when Answering the call");
+ throw new AzureCallAutomationException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public String startRecording(final EventInfo eventInfo) {
+ try {
+ ServerCallLocator serverCallLocator = new ServerCallLocator(callAutomationClientFactory
+ .getCallAutomationClient(eventInfo.getCorrelationId())
+ .getCallConnection(eventInfo.getCallConnectionId())
+ .getCallProperties()
+ .getServerCallId());
+
+ StartRecordingOptions recordingOptions = new StartRecordingOptions(serverCallLocator);
+
+ Response response = callAutomationClientFactory.getCallAutomationClient(eventInfo.getCorrelationId())
+ .getCallRecording()
+ .startWithResponse(recordingOptions, Context.NONE);
+ String recordingId = response.getValue().getRecordingId();
+ log.info("Start Recording with recording ID: {}", recordingId);
+ return "Start Recording operation finished";
+ } catch(Exception e) {
+ log.error("Recording operation failed {} {}", e.getMessage(), e.getCause());
+ throw new AzureCallAutomationException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public String playAudio(final EventInfo eventInfo, final String prompt) {
+ List listTargets = Arrays.asList(CommunicationIdentifier.fromRawId(eventInfo.getFromId()));
+ PlaySource playSource = new FileSource().setUrl(acsConfig.getMediaUri(prompt));
+ PlayOptions playOptions = new PlayOptions(playSource, listTargets);
+ log.info("Play audio operation started");
+ try {
+ Response response = callAutomationClientFactory.getCallAutomationClient(eventInfo.getCorrelationId())
+ .getCallConnection(eventInfo.getCallConnectionId())
+ .getCallMedia()
+ .playWithResponse(playOptions, Context.NONE);
+ return "Play audio operation finished";
+ } catch(Exception e) {
+ log.error("Error when Playing audio to participant {} {}", e.getMessage(), e.getCause());
+ throw new AzureCallAutomationException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public String singleDigitDtmfRecognitionWithPrompt(final EventInfo eventInfo, final String prompt) {
+ CommunicationIdentifier rectarget = CommunicationIdentifier.fromRawId(eventInfo.getFromId());
+ PlaySource playSource = new FileSource().setUrl(acsConfig.getMediaUri(prompt));
+ CallMediaRecognizeDtmfOptions recognizeDtmfOptions = new CallMediaRecognizeDtmfOptions(rectarget, 1);
+ recognizeDtmfOptions.setInterToneTimeout(Duration.ofSeconds(10))
+ .setInitialSilenceTimeout(Duration.ofSeconds(15))
+ .setInterruptPrompt(true)
+ .setPlayPrompt(playSource);
+ log.info("DTMF Recognition operation started");
+ try {
+ Response response = callAutomationClientFactory.getCallAutomationClient(eventInfo.getCorrelationId())
+ .getCallConnection(eventInfo.getCallConnectionId())
+ .getCallMedia()
+ .startRecognizingWithResponse(recognizeDtmfOptions, Context.NONE);
+ return "DTMF Recognition operation ended";
+ } catch(Exception e) {
+ log.error("DTMF Recognition operation failed {} {}", e.getMessage(), e.getCause());
+ throw new AzureCallAutomationException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public String terminateCall(final EventInfo eventInfo) {
+ log.info("Terminating the call");
+ try {
+ callAutomationClientFactory.getCallAutomationClient(eventInfo.getCorrelationId())
+ .getCallConnection(eventInfo.getCallConnectionId())
+ .hangUp(true);
+ return "HangUp call for all participants operation ended";
+ } catch(Exception e) {
+ log.error("HangUp call for all participants operation failed {} {}", e.getMessage(), e.getCause());
+ throw new AzureCallAutomationException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/acs/client/CallAutomationClientFactory.java b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/acs/client/CallAutomationClientFactory.java
new file mode 100644
index 0000000..d322d04
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/acs/client/CallAutomationClientFactory.java
@@ -0,0 +1,7 @@
+package com.communication.callautomation.acs.client;
+
+import com.azure.communication.callautomation.CallAutomationClient;
+
+public interface CallAutomationClientFactory {
+ CallAutomationClient getCallAutomationClient(final String correlationId);
+}
diff --git a/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/acs/client/CallAutomationClientImpl.java b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/acs/client/CallAutomationClientImpl.java
new file mode 100644
index 0000000..cc2e37a
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/acs/client/CallAutomationClientImpl.java
@@ -0,0 +1,51 @@
+package com.communication.callautomation.acs.client;
+
+import com.azure.communication.callautomation.CallAutomationAsyncClient;
+import com.azure.communication.callautomation.CallAutomationClient;
+import com.azure.communication.callautomation.CallAutomationClientBuilder;
+import com.azure.core.http.HttpClient;
+import com.communication.callautomation.config.AcsConfig;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+@Slf4j
+public class CallAutomationClientImpl implements CallAutomationClientFactory {
+ private final AcsConfig acsConfig;
+ private final HttpClient azureHttpClient;
+ private final Map clientMap = new ConcurrentHashMap();
+
+ @Autowired
+ public CallAutomationClientImpl(final AcsConfig acsConfig,
+ final HttpClient azureHttpClient) {
+ this.acsConfig = acsConfig;
+ this.azureHttpClient = azureHttpClient;
+ }
+ @Override
+ public CallAutomationClient getCallAutomationClient(final String correlationId) {
+ log.debug("Start: getCallAutomationClient");
+ String connectionString = acsConfig.getConnectionString();
+ CallAutomationClient callAutomationClient;
+ callAutomationClient = clientMap.get(correlationId);
+ if (callAutomationClient == null) {
+ callAutomationClient = createCallAutomationClient(connectionString, correlationId);
+ }
+ log.debug("End: getCallAutomationClient");
+ return callAutomationClient;
+ }
+
+ private synchronized CallAutomationClient createCallAutomationClient(final String connectionString, final String correlationId) {
+ log.debug("Start: createCallAutomationClient");
+ CallAutomationClientBuilder callAutomationClientBuilder = new CallAutomationClientBuilder()
+ .httpClient(azureHttpClient)
+ .connectionString(connectionString);
+ CallAutomationClient callAutomationClient = callAutomationClientBuilder.buildClient();
+ clientMap.put(correlationId, callAutomationClient);
+ log.debug("End: createCallAutomationClient");
+ return callAutomationClient;
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/config/AcsConfig.java b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/config/AcsConfig.java
new file mode 100644
index 0000000..2af5826
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/config/AcsConfig.java
@@ -0,0 +1,57 @@
+package com.communication.callautomation.config;
+
+import com.azure.core.implementation.util.EnvironmentConfiguration;
+import com.azure.core.util.Configuration;
+import com.communication.callautomation.controller.ApiVersion;
+import lombok.Getter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.bind.ConstructorBinding;
+
+import java.util.UUID;
+
+@ConfigurationProperties(prefix = "acs")
+@Getter
+public class AcsConfig {
+ private final String connectionString;
+ private final String basecallbackuri;
+ private final String timeoutMs;
+ private final String mediarecordingstarted;
+ private final String mediamainmenu;
+ private final String mediaretry;
+ private final String mediagoodbye;
+ private final String mediachoice1;
+ private final String mediachoice2;
+ private final String mediachoice3;
+
+ @ConstructorBinding
+ AcsConfig(final String connectionString,
+ final String basecallbackuri,
+ final String timeoutMs,
+ final String mediarecordingstarted,
+ final String mediamainmenu,
+ final String mediaretry,
+ final String mediagoodbye,
+ final String mediachoice1,
+ final String mediachoice2,
+ final String mediachoice3) {
+ this.connectionString = connectionString;
+ this.basecallbackuri = basecallbackuri;
+ this.timeoutMs = timeoutMs;
+ this.mediarecordingstarted = mediarecordingstarted;
+ this.mediamainmenu = mediamainmenu;
+ this.mediaretry = mediaretry;
+ this.mediagoodbye = mediagoodbye;
+ this.mediachoice1 = mediachoice1;
+ this.mediachoice2 = mediachoice2;
+ this.mediachoice3 = mediachoice3;
+ EnvironmentConfiguration.getGlobalConfiguration().put(Configuration.PROPERTY_AZURE_REQUEST_RESPONSE_TIMEOUT, timeoutMs);
+ }
+
+ public String getCallbackUri(String callerId){
+ return basecallbackuri + String.format("%s/calls/ongoing/%s?callerId=%s", ApiVersion.CURRENT, UUID.randomUUID(), callerId);
+ }
+
+ public String getMediaUri(String filename){
+ return basecallbackuri + String.format("%s/calls/media/%s", ApiVersion.CURRENT, filename);
+ }
+}
diff --git a/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/controller/ApiVersion.java b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/controller/ApiVersion.java
new file mode 100644
index 0000000..048f37f
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/controller/ApiVersion.java
@@ -0,0 +1,7 @@
+package com.communication.callautomation.controller;
+
+public class ApiVersion {
+ private ApiVersion() {}
+
+ public static final String CURRENT = "/v1";
+}
diff --git a/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/controller/CallController.java b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/controller/CallController.java
new file mode 100644
index 0000000..6ede769
--- /dev/null
+++ b/CallAutomation_AppointmentBooking/src/main/java/com/communication/callautomation/controller/CallController.java
@@ -0,0 +1,59 @@
+package com.communication.callautomation.controller;
+
+import com.azure.messaging.eventgrid.EventGridEvent;
+import com.communication.callautomation.exceptions.InvalidEventPayloadException;
+import com.communication.callautomation.exceptions.MediaLoadingException;
+import com.communication.callautomation.handler.EventsHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.util.List;
+
+@RestController
+@RequestMapping(ApiVersion.CURRENT + "/calls")
+@Slf4j
+public class CallController {
+ private EventsHandler eventsHandler;
+
+ public CallController(final EventsHandler eventsHandler) { this.eventsHandler = eventsHandler; }
+
+ @PostMapping(path = "/incoming", produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity incomingCallEndpoint(@RequestBody final String reqBody) {
+ List events;
+ try{
+ events = EventGridEvent.fromString(reqBody);
+ } catch (IllegalArgumentException ex) {
+ throw new InvalidEventPayloadException(ex.getMessage(), ex);
+ }
+ log.trace("Request received at CallController");
+ return eventsHandler.handleIncomingEvents(events);
+ }
+
+ @PostMapping(path = "/ongoing/{contextId}", produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity ongoingCallEndpoint(@RequestBody final String reqBody,
+ @PathVariable final String contextId,
+ @RequestParam(name = "callerId") String callerId) {
+ JSONArray events;
+ try{
+ events = new JSONArray(reqBody);
+ } catch (JSONException ex) {
+ throw new InvalidEventPayloadException(ex.getMessage(), ex);
+ }
+ log.trace("Ongoing events received at CallController");
+ return eventsHandler.handleOngoingEvents(reqBody);
+ }
+
+ @GetMapping(path = "/media/{filename}", produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity