The REST Demonstration includes the components described below.
Metaswitch Call Notification REST API
The Rhino REST API Framework was used to create the MSW-CN REST API implementation (Creating a REST API). The generated module structure is:
msw-call-notification-rest-api msw-call-notification-rest-api/msw-call-notification-api/src/api.yaml msw-call-notification-rest-api/oma-call-notification-api/src/api.properties msw-call-notification-rest-api/msw-call-notification-api msw-call-notification-rest-api/msw-call-notification-api-ratype msw-call-notification-rest-api/msw-call-notification-api-plugin msw-call-notification-rest-api/msw-call-notification-api-client-sbbpart
-
The MSW-CN group module
-
The MSW-call-notification-api sub-module holds the API specification and a properties file. The other sub-modules have an ivy dependency on this module.
-
An OpenAPI formatted description of the MSW-CN API
-
A properties file that defines java packages, and the names of deployable units
-
Generates a RA type that includes events, provider interfaces and so on for a MSW-CN RA Type that can be deployed in Rhino
-
Generates a library component the Unified REST RA uses to load and drive the MSW-CN API
-
Generates a Jar containing a super-class that developers extend to create an sbb-part, for the client MSW-CN role, in their application. The generated super-class includes all event handlers and slee-annotations required to deploy such an sbb-part in Rhino
Developers may create multiple REST API modules. A REST RA can support one or more such REST APIs. |
Call Notification Resource Adaptor
The Rhino REST API Framework was used to create the Call Notification Resource Adaptor (Creating a REST Resource Adaptor).
The REST Demonstration REST resource adaptor module is: call-notification-ra
The call-notification-ra
is an Ant/Ivy build module that depends on the Unified REST RA Core and the Metaswitch Call Notification REST API.
The call-notification-ra
produces a Unified REST RA, that supports MSW-CN, used in Sentinel-SIP.
Developers may create multiple REST RA modules, each with different sets of APIs to suit different applications. |
Call Notification Client Event Handler
The MSW-CN event handler sbbpart (rest-demo-feature-modules/msw-call-notification-client-event-handler
) contains an sbb-part that extends the class generated in msw-call-notification-rest-api/msw-call-notification-api-client-sbbpart
.
The MswCallNotificationClientEventHandlerSbbpart
class extends the MswCallNotificationApiClientSbbpart
class and implements behaviour on receipt of call direction and call event notification response events.
The super class includes the required SBBPart
, RATypeBinding
and EventMethod
slee annotations.
package com.opencloud.openapi.demo;
// msw call notification API
import com.opencloud.openapi.demo.msw_cn.api.*;
// super-class for an sbb-part to process
// msw call notification related response events
import com.opencloud.openapi.demo.msw_cn.impl.*;
// to bind the event handler sbbpart to the sentinel-sip service
@BinderTargets(services="sip")
public class MswCallNotificationClientEventHandlerSbbpart extends MswCallNotificationClientSbbpart {
public MswCallNotificationClientEventHandlerSbbpart(SbbPartContext context) { super(context); }
@Override
protected void handleCallDirectionNotification_200_SuccessResponse(
CallDirectionNotification_200_SuccessResponse response,
ActivityContextInterface aci,
EventContext eventContext) throws IOException
{
submitResponseEventToSentinel(
"CallDirectionNotification", response, response.getStatusCode(),
"SuccessResponse", aci, eventContext);
}
@Override
protected void handleCallDirectionNotificationDefaultResponse(
CallDirectionNotificationDefaultResponse response,
ActivityContextInterface aci,
EventContext eventContext) throws IOException
{
submitResponseEventToSentinel(
"CallDirectionNotification", response, response.getStatusCode(),
"DefaultResponse", aci, eventContext);
}
@Override
protected void handleCallEventNotification_200_SuccessResponse(
CallEventNotification_200_SuccessResponse response,
ActivityContextInterface aci,
EventContext eventContext) throws IOException
{
submitResponseEventToSentinel(
"CallEventNotification", response, response.getStatusCode(),
"SuccessResponse", aci, eventContext);
}
@Override
protected void handleCallEventNotificationDefaultResponse(
CallEventNotificationDefaultResponse response,
ActivityContextInterface aci,
EventContext eventContext) throws IOException
{
submitResponseEventToSentinel(
"CallEventNotification", response, response.getStatusCode(),
"DefaultResponse", aci, eventContext);
}
// Submit response event to sentinel runtime
// for delivery to a feature for processing
private void submitResponseEventToSentinel(
String operation, Object response, int statusCode,
String description,
ActivityContextInterface aci, EventContext eventContext)
{
final SentinelActivityContextInterface sentinelAci =
getSbbPartContext().asSbbPartActivityContextInterface(aci);
final String target = sentinelAci.getTargetFeature();
if (target == null || target.isEmpty()) {
getTracer().fine("No target feature for response, ignore");
return;
}
getTracer().finer("Pass {} {} {} to target feature \"{}\"",
operation, statusCode, description, target);
try {
SentinelEndpoint sentinelEndpoint =
(SentinelEndpoint) getSbbPartContext().getSbbLocalObject();
sentinelEndpoint.processEvent(
target, response, false, true, aci, eventContext);
}
catch (SentinelFireEventException | IllegalArgumentException | NullPointerException e)
{
getTracer().severe("Exception processing {} {} {} event",
operation, statusCode, description, e);
}
}
}
-
override method in
MswCallNotificationClientSbbpart
to handle a CallDirectionNotification 200 Success response -
override method in
MswCallNotificationClientSbbpart
to handle a CallDirectionNotification default response (i.e all other CallDirectionNotification responses) -
override method in
MswCallNotificationClientSbbpart
to handle a CallEventNotification 200 Success response -
override method in
MswCallNotificationClientSbbpart
to handle a CallEventNotification default response (i.e all other CallEventNotification responses) -
find the feature the response event should be processed by
-
submit the event to the Sentinel-SIP runtime for processing by the target feature
Set Session Plan Feature
The RestSetSessionPlan
feature (rest-demo-feature-modules/msw-set-session-plan-feature
) updates the sentinel selection key PlanID
field with the REST Demonstration session plan ID.
Learn about the Sentinel Selection Key and read Features for an introduction to Sentinel features. |
package com.opencloud.openapi.demo;
@SentinelFeature(
featureName = RestSetSessionPlan.NAME,
componentName = "@component.name@",
featureVendor = "@component.vendor@",
featureVersion = "@component.version@",
featureGroup = SentinelFeature.SIP_FEATURE_GROUP,
executionPhases = ExecutionPhase.SipSessionPhase,
usageStatistics = RestSetSessionPlanStats.class
)
// to bind the feature to the sentinel-sip service
@BinderTargets(services = "sip")
public class RestSetSessionPlan
extends BaseFeature<SentinelSipSessionState, SentinelSipMultiLegFeatureEndpoint>
implements InjectFeatureStats<RestSetSessionPlanStats>
{
public static final String NAME = "RestSetSessionPlan";
// ...
@Override
public void startFeature(Object trigger, Object activity, ActivityContextInterface aci) {
final SentinelSelectionKey selectionKey = getSessionState().getSentinelSelectionKey();
// Set the plan ID in the selectionKey to 'rest_demo'
selectionKey.setPlanId(REST_DEMO_SESSION_PLAN);
getTracer().finest("SentinelSelectionKey set: {}", selectionKey);
getSessionState().setSentinelSelectionKey(selectionKey);
getCaller().featureHasFinished();
}
private static final String REST_DEMO_SESSION_PLAN = "rest_demo";
}
The value of the SentinelSelectionKey
is used by the Sentinel run-time to resolve feature configuration, and the feature execution scripts that run.
There are many alternate approaches to deriving a plan ID. For example the plan ID could be derived by analysing headers in the SIP INVITE. |
Session plans, the Sentinel-SIP session plan and feature execution scripts are explained here: Session Plans, SIP Sessions Initiated via SIP INVITE and Feature Execution Scripts. |
Send Call Notification Feature
The RestSendCallNotification
feature (rest-demo-feature-modules/msw-send-call-notification
) sends CallDirection and CallEvent notification requests and processes the corresponding responses.
The RestSendCallNotification feature:
-
determines the type of notification event to send, if appropriate, based on the point-in-session and the triggering SIP event.
-
will influence the current SIP session based on CallDirection response events. For example the session may be ended, routed to an alternate destination or allowed to continue.
@Override
public void startFeature(Object trigger,
Object activity,
ActivityContextInterface aci)
{
// Find the active point-in-session
final SipFeatureScriptExecutionPoint executionPoint =
getSessionState().getCurrentSipFeatureExecutionPoint();
if (trigger instanceof IncomingSipRequest
|| trigger instanceof IncomingSipResponse)
{
// Determine the notification type and call event
final CallNotificationTypes notificationType;
final CallEvents callEvent;
if (trigger instanceof IncomingSipRequest) {
final IncomingSipRequest request = (IncomingSipRequest) trigger;
notificationType = determineNotificationType(
executionPoint, request);
callEvent = determineCallEvent(executionPoint, request);
}
else {
final IncomingSipResponse response = (IncomingSipResponse) trigger;
notificationType = determineNotificationType(
executionPoint, response);
callEvent = determineCallEvent(executionPoint, response);
}
if (null == notificationType || null == callEvent) {
getCaller().featureHasFinished();
return;
}
final LegManager lm = getCaller().getLegManager();
final Leg calledPartyLeg = lm.getCallingPartyLeg().getLinkedLeg();
// if there is no linked leg, then this response will have been received
// on a leg associated with an MRF (for an announcement).
// A notification is not required in this case.
if (null == calledPartyLeg) {
final Leg leg = getCaller().getLegManager().getLeg(aci);
getTracer().finest("A notification is not required at: {} for response on leg {}",
executionPoint, leg.getLegName());
getCaller().featureHasFinished();
return;
}
// send a notification
// if the leg is not active, a sip session has not been created yet
// if there is no INVITE request, then an INVITE has not
// been sent on the cld leg yet
if (calledPartyLeg.isActive()
&& null != calledPartyLeg.getInviteRequest())
{
sendNotification(calledPartyLeg, notificationType, callEvent);
}
else {
sendNotification(aci, notificationType, callEvent);
}
}
else {
// Process a notification response event
if (processNotificationResponse(
executionPoint, trigger, activity, aci))
{
getCaller().featureHasFinished();
}
getCaller().detach(aci);
}
}
-
Find the currently execution feature script execution point.
-
Determine the notification type and call event based on the triggering SIP message, and the point-in-session
-
Send a notification request message
-
Process a
CallDirection
orCallEvent
notification response event.
Determining the MSW-CN notification event to send
// Determine the notification type from the triggering SIP request
private CallNotificationTypes determineNotificationType(
SipFeatureScriptExecutionPoint executionPoint,
IncomingSipRequest request)
{
switch (executionPoint) {
// the initial trigger
case SipAccess_SessionStart:
return CallNotificationTypes.CALLDIRECTION;
// one of the participants hangs up
case SipMidSession_PartyRequest:
return "BYE".equals(request.getMethod())
? CallNotificationTypes.CALLDIRECTION : null;
// a notification is not required
default: return null;
}
}
// Determine the notification type from the triggering SIP response
private CallNotificationTypes determineNotificationType(
SipFeatureScriptExecutionPoint executionPoint,
IncomingSipResponse response)
{
switch (executionPoint) {
case SipAccess_PartyResponse:
if (! "INVITE".equals(response.getRequest().getMethod()))
return null;
switch (response.getStatus()) {
case SipResponse.SC_OK:
return CallNotificationTypes.CALLEVENT;
// 486 - called party busy
case SipResponse.SC_BUSY_HERE:
// 404 - no route to destination
case SipResponse.SC_NOT_FOUND:
// 408 - no answer
case SipResponse.SC_REQUEST_TIMEOUT:
// 480 - no answer
case SipResponse.SC_TEMPORARLY_UNAVAILABLE:
return CallNotificationTypes.CALLDIRECTION;
// a notification is not required
default: return null;
}
case SipMidSession_PartyResponse:
if (SipResponse.SC_OK == response.getStatus()
&& "BYE".equals(response.getRequest().getMethod()))
return CallNotificationTypes.CALLEVENT;
// otherwise a notification is not required
return null;
// a notification is not required
default: return null;
}
}
-
At
SipAccess_SessionStart
Sentinel-SIP is processing a triggeringINVITE
request, so send aCallNotificationTypes.CALLDIRECTION
request. AtSipMidSession_PartyRequest
we are interested inBYE
requests and send aCallNotificationTypes.CALLDIRECTION
request. -
At
SipAccess_PartyResponse
we are interested in responses to the triggeringINVITE
request. On answer, send aCallNotificationTypes.CALLEVENT
request, otherwise send aCallNotificationTypes.CALLDIRECTION
request. AtSipMidSession_PartyResponse
we are interested in responses toBYE
requests and send aCallNotificationTypes.CALLEVENT
request.
// Determine the CallEvent from the triggering SIP request
private CallEvents determineCallEvent(
SipFeatureScriptExecutionPoint executionPoint,
IncomingSipRequest request)
{
switch (executionPoint) {
// the initial trigger
case SipAccess_SessionStart:
return CallEvents.CALLEDNUMBER;
// one of the participants hangs up
case SipMidSession_PartyRequest:
return "BYE".equals(request.getMethod())
? CallEvents.DISCONNECTED : null;
// a notification is not required
default: return null;
}
}
// Determine the CallEvent from the triggering SIP response
private CallEvents determineCallEvent(
SipFeatureScriptExecutionPoint executionPoint,
IncomingSipResponse response)
{
switch (executionPoint) {
case SipAccess_PartyResponse:
if (! "INVITE".equals(response.getRequest().getMethod()))
return null;
switch (response.getStatus()) {
case SipResponse.SC_OK:
return CallEvents.ANSWER;
// 486 - called party busy
case SipResponse.SC_BUSY_HERE:
return CallEvents.BUSY;
// 404 - no route to destination
case SipResponse.SC_NOT_FOUND:
return CallEvents.NOTREACHABLE;
// 408 - no answer
case SipResponse.SC_REQUEST_TIMEOUT:
// 480 - no answer
case SipResponse.SC_TEMPORARLY_UNAVAILABLE:
return CallEvents.NOANSWER;
// a notification is not required
default: return null;
}
case SipMidSession_PartyResponse:
if (SipResponse.SC_OK == response.getStatus()
&& "BYE".equals(response.getRequest().getMethod()))
return CallEvents.DISCONNECTED;
// otherwise a notification is not required
return null;
// a notification is not required
default: return null;
}
}
-
At
SipAccess_SessionStart
Sentinel-SIP is processing a triggeringINVITE
request (aCallEvents.CALLEDNUMBER
event). AtSipMidSession_PartyRequest
we are interested inBYE
requests (CallEvents.DISCONNECTED
events) -
At
SipAccess_PartyResponse
we are interested in responses to the triggeringINVITE
request (CallEvents.ANSWER
,CallEvents.BUSY
and so on). AtSipMidSession_PartyResponse
we are interested in responses toBYE
requests (CallEvents.DISCONNECTED
events).
Sending CallDirection and CallEvent notification requests
The RestSendCallNotification
feature sends CallDirection
and CallEvent
notification requests in the following way.
// Send a call event notification
private void sendNotification(
String from, String fromDisplayName, String to,
CallNotificationTypes notificationType, CallEvents callEvent)
{
final SendCallNotificationConfig configuration =
configurationReader.getConfiguration(
getSessionState().getSentinelSelectionKey());
final ApiConfiguration apiConfiguration =
ApiConfiguration.standardConfiguration();
apiConfiguration.set(ApiOptions.destinationUrl,
configuration.getDestinationUrl());
final MswCallNotificationRaTypeApiClient client =
cnProvider.getApiClient(apiConfiguration);
// Create a new CallEventNotification object
final CallEventNotification notification = new CallEventNotification();
try {
notification.notificationType(notificationType)
.eventDescription(new EventDescription().callEvent(callEvent))
.calledParticipant(new URI(to))
.callingParticipant(new URI(from))
.callingParticipantName(fromDisplayName);
}
catch (URISyntaxException e) {
getTracer().severe(
"Failed to extract participants from: {}, to: {}", from, to, e);
}
// Create a REST request to send
final NotificationApi nApi = client.getNotificationApi();
final RestRequestBuilder notificationRequest =
notificationType == CallNotificationTypes.CALLEVENT
? nApi.createCallEventNotificationRequest(notification)
: nApi.createCallDirectionNotificationRequest(notification);
try {
// Send the REST request
final OutgoingRestActivity outgoingRestActivity =
notificationApi.sendRequest(notificationRequest);
final ActivityContextInterface newAci =
cnACIFactory.getActivityContextInterface(outgoingRestActivity);
final SentinelActivityContextInterface outgoingAci =
getCaller().asSentinelActivityContextInterface(newAci);
stats.incrementSendCallNotification(1);
// Set the target feature to 'RestSendCallNotification'
outgoingAci.setTargetFeature(NAME);
// Attach to the new activity, wait for the response
outgoingAci.attach(getFacilities().getSbbLocalObject());
getTracer().finest("Notification sent. Waiting for response");
getCaller().featureWaiting(outgoingAci);
}
catch (IOException e) {
getCaller().featureFailedToExecute(
new FeatureError(FeatureError.Cause.unclassified,
"Failed to send notification", e));
}
}
-
The
CallEventNotification
has:-
a notification type (
CallNotificationTypes.CALLEVENT
orCallNotificationTypes.CALLDIRECTION)
-
a call event (
CallEvents.CALLEDNUMBER
,CallEvents.ANSWER
and so on) -
called participant (the
To
address), calling participant (theFrom
address) and the calling participant display name
-
-
Create an appropriate REST request message by using the MSW-CN
NotificationApi
. -
Send the REST request. The Unified REST RA returns an
OutgoingRestActivity
related to the sent REST request. -
Set
TargetFeature
on theActivityContextInterface
toRestSendCallNotification
so Sentinel-SIP will route the corresponding response to the currently executing feature. -
Attach to the activity and tell Sentinel-SIP that we are waiting for a response. Sentinel-SIP pauses executing the active feature execution script until a response event arrives.
Processing CallDirection 200 Success response events
A CallDirectionNotification
success response includes instructions that must be followed to effect ongoing call processing.
private boolean processDirectionNotificationSuccessResponse(
SipFeatureScriptExecutionPoint executionPoint,
CallDirectionNotification_200_SuccessResponse trigger,
Object activity)
{
// Determine the action to perform
final Action actionToTake = trigger.getContent();
if (null == actionToTake || null == actionToTake.getActionToPerform()) {
stats.incrementCallDirectionResponseWithNoActionToTake(1);
return true;
}
// Apply the requested Action
switch(actionToTake.getActionToPerform()) {
case ROUTE:
// Get the new destination
final URI newDestination = actionToTake.getRoutingAddress();
if (null == newDestination) {
stats.incrementCallDirectionNoRoutingAddress(1);
return true;
}
final org.jainslee.resources.sip.URI sipDestination =
getSipUri(newDestination);
if (null == sipDestination) {
stats.incrementCallDirectionNonSipRoutingAddress(1);
return true;
}
stats.incrementCallDirectionRoute(1);
// route the call to the new destination
routeToNewDestination(executionPoint, sipDestination);
return true;
case CONTINUE:
// Allow the call to continue without interruption
stats.incrementCallDirectionContinue(1);
return true;
case ENDCALL:
// End the call
stats.incrementCallDirectionEndCall(1);
// Check to see if an annoucement should be played
final String announcementIdStr = actionToTake.getAnnouncementId();
if (null == announcementIdStr) {
getTracer().fine("Action is to ENDCALL");
// defer to the leg manager to end the session
final LegManager lm = getCaller().getLegManager();
lm.endSession(SipResponse.SC_FORBIDDEN);
}
else {
// Schedule an announcement
try {
// announcement ID should be a Long.
final long announcementId = Long.parseLong(announcementIdStr);
getTracer().fine("Action is to ENDCALL after playing announcement: {}",
announcementIdStr);
// tell sentinel-sip about the announcement to be played
final List<SipAnnouncementInformation> announcementQueue =
getSessionState().getEarlyMediaAnnouncementInfoQueue();
announcementQueue.add(SipAnnouncementInformation.getBuilder()
.setAnnouncementId(announcementId).build());
getSessionState().setEarlyMediaAnnouncementInfoQueue(announcementQueue);
// tell sentinel-sip to end the session after the announcement
getSessionState().setEndSessionAfterAnnouncement(SipResponse.SC_FORBIDDEN);
// used in rest-demo feature execution scripts
getSessionState().setPlayAnnouncementThenEndSession(true);
}
catch (NumberFormatException nfe) {
getTracer().fine("Announcement id: {} is not a valid. Ending the call.", announcementIdStr);
// defer to the leg manager to end the session
final LegManager lm = getCaller().getLegManager();
lm.endSession(SipResponse.SC_FORBIDDEN);
}
}
// generate a DISCONNECTED notification
sendNotification(
lm.getCallingPartyLeg(),
CallNotificationTypes.CALLEVENT,
CallEvents.DISCONNECTED);
return false; // sent a notification, feature has not finished
}
return true;
}
-
The
CallDirectionNotification
200 success response body contains anAction
object that dictates how the active SIP session should be processed. -
The
ActionToPerform
is one ofROUTE
,CONTINUE
orENDCALL
-
If the action to perform is
ROUTE
theactionToTake
will also include a new destination address. -
Route the call to the new adddress.
-
If the action to perform is
CONTINUE
allow the call to continue uninterrupted. -
If the action to perform is
ENDCALL
delegate to Sentinel-SIP. -
The
actionToTake
may, optionally, include an announcement ID -
If
actionToTake
includes an announcement ID, then update session state, so thePlayAnnouncement
feature can play the announcement -
Generate a
DISCONNECTED
notification
Learn more about the SIP Play Announcement Feature. |
Feature Execution Scripts
The RestSetSessionPlan
feature (rest-demo-feature-modules/msw-set-session-plan-feature
) updates the sentinel selection key to with the REST Demonstration session plan.
Sentinel-SIP uses the sentinel-selection-key when resolving the feature execution scripts to run at each point in session.
For more details on the sentinel selection key, feature execution scripts and Sentinel-SIP feature script execution points see: Sentinel Selection Key Feature Execution Scripts and Sentinel SIP Feature Execution Points |
The following feature execution scripts have been added for the REST Demonstration.
Point-in-Session | feature Execution Script |
---|---|
|
featurescript RestSessionStart { if not LegManager.endSession and not LegManager.detachAll and not LegManager.currentLeg.releaseLeg { run AcceptSip run RestSetSessionPlan run DoNotChargeSipSession } } |
|
featurescript DemoPostSessionStart { if not LegManager.endSession { run RecordTimestamps mode "outbound" run RestSendCallNotification if session.PlayAnnouncementThenEndSession { run SipPlayAnnouncement } } run DetermineCauseCode run DiameterServiceInfo run DiameterPerLegInfo mode "outbound" } |
|
featurescript RestDemoSessionCheck { if not session.PlayAnnouncementThenEndSession { run SipSubscriberDetermination run CallPartiesAddressDetermination run SessionTracing } } |
|
featurescript RestDemoSubscriberCheck { if not session.PlayAnnouncementThenEndSession { run SubscriberDataLookup run SubscriberValidity } } |
|
featurescript RestSipAccessPartyRequest { run RestSendCallNotification if session.PlayAnnouncementThenEndSession { run SipPlayAnnouncement } } |
|
featurescript RestSipAccessPartyResponse { run RestSendCallNotification if session.PlayAnnouncementThenEndSession { run SipPlayAnnouncement } } |
|
featurescript DemoMidSessionPartyRequest { run RestSendCallNotification if session.PlayAnnouncementThenEndSession { run SipPlayAnnouncement } } |
|
featurescript DemoMidSessionPartyResponse { run RestSendCallNotification if session.PlayAnnouncementThenEndSession { run SipPlayAnnouncement } } |