Skip to main content

Sample Red App using workflow extension - walkthrough

The goal of this sample is to narrow the results of LFS to the required airline. When LFS command is given the front-end will show a popup and will ask the user to provide an airline code. When the user submits the necessary input then the LFS command will be modified to narrow results to be only from the provided airline.  Secondly, It shows how to acquire the PQR number from the model in the afterExchangeConfirmation extension point, and how to pass it further.

In Java:
  1. Create a Red App plugin in the same way as an ordinary Red App.

  2. Create a Java data model. This data model is responsible for transferring data from the back-end to front-end and back. Based on this data model, the front-end will show a view for the gathering user’s input. When the user will submit their input, then on the back-end side this data model will contain the provided data. Class must be annotated in a way that allows JAXB to transform it to JSON and back to Java Object. 

image

Airline.java: 

package com.sabre.redapp.example3.workflow.extension.data;
 
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

/
 * Java class for Airline complex type.
 */

@XmlAccessorType(XmlAccessType.FIELD)

@XmlType(name = "Airline", namespace =
"http://example.redapp.sabre.com/humantask/workflow/extension/data",
                propOrder = \{"iataCode"})
@XmlRootElement(name = "Airline",  namespace =
"http://example.redapp.sabre.com/humantask/workflow/extension/data")

public class Airline implements Serializable
{ +
    private static final long serialVersionUID = 1L;
    @XmlElement(namespace = +
"http://example.redapp.sabre.com/humantask/workflow/extension/data",
                    required = true)
    protected String iataCode;

Container.java: 

package com.sabre.redapp.example3.workflow.extension.data;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

/**
 * Container class
 */
@XmlRootElement(name = "Container", namespace = "http://example.redapp.sabre.com/humantask/workflow/extension/data")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Container", namespace = "http://example.redapp.sabre.com/humantask/workflow/extension/data",
    propOrder = {"value", "extpoint", "info"})
public class Container
{
    @XmlElement(namespace = "http://example.redapp.sabre.com/humantask/workflow/extension/data",
		name = "value", required = true)
    protected String value;
    @XmlElement(namespace = "http://example.redapp.sabre.com/humantask/workflow/extension/data",
		name = "extpoint", required = true)
    protected String extpoint;
    @XmlElement(namespace = "http://example.redapp.sabre.com/humantask/workflow/extension/data",
		name = "info", required = true)
    protected String info;

3.     Next, add namespace mapping in transformer.properties file.

image

 

transformer.properties:

pkg.com_sabre_redapp_example_workflow_extension=com.sabre.redapp.example3.workflow.extension.data
ns.com_sabre_redapp_example_workflow_extension=http://example.redapp.sabre.com/humantask/workflow/extension/data

File transformer properties must contain valid package mapping – prefix pkg and valid namespace mapping – prefix ns. Both names must be the same and must contain only alphanumeric characters and underscore(_).

4.  Add data model classes into ObjectFactory class.

image

ObjectFactory.java:

package com.sabre.redapp.example3.workflow.extension.data; 

import javax.xml.bind.annotation.XmlRegistry; 

/
 * This object contains factory methods for each Java content interface
and Java
 * element interface generated in the
 * com.sabre.redapp.example.workflow.extension.data package.
 */

@XmlRegistry
public class ObjectFactory
{
    /
     * Create a new ObjectFactory that can be used to create new instances of
     * schema derived classes for package:
     * com.sabre.redapp.example.workflow.extension.data
     */

    public ObjectFactory()
    {

    }

    /
     * Create an instance of {@link Airline }
     * @return instance of {@link Airline }
     */
    public Airline createAirline()
    {
      return new Airline();
    }
}

 

5.      Create Eclipse service which will provide data model for front-end:

a.)    Interface.  It must contain method with one argument, which is type of FlowExtPointCommand and return same FlowExtPointCommand type.

 

image

 

DisplayUpdateAirlineFormService.java:

package com.sabre.redapp.example3.workflow.extension.service;

import com.sabre.stl.pos.srw.nextgen.flow.ext.v2.FlowExtPointCommand; 

/
 * Service displaying form for updating command
 */
public interface DisplayUpdateAirlineFormService
{
    /
     * Prepares data model to display updated command form.
     * @param extPointCommand \{@link FlowExtPointCommand} flow
     * @return {@link FlowExtPointCommand} flow
     */
    FlowExtPointCommand execute(FlowExtPointCommand extPointCommand);
}

 

b.)    Implementation

 

image

 

DisplayUpdateAirlineFormServiceImpl.java:

/
 * Service displaying form for updating command.
 */
public class DisplayUpdateAirlineFormServiceImpl implements DisplayUpdateAirlineFormService
{
    @Override
    public FlowExtPointCommand execute(FlowExtPointCommand extPointCommand)
    {
        Airline airline = new Airline();
        airline.setIataCode("");

        FlowExtPointResponse response = new FlowExtPointResponse();
        response.setStructure(airline); 

        FlowExtPointResponseWrapper responseWrapper =new FlowExtPointResponseWrapper();
        responseWrapper.setOperation(FlowExtPointDataOperation.ADD);
        responseWrapper.setResponse(response);
        extPointCommand.getResponses().add(responseWrapper);

        return extPointCommand;
    }
}

 

c.)    Service declaration file in the OSG-INF directory. This file must contain:

-        Unique name

-        Full path to class implementation

-        Full path to interface which will provide service functionalities.

 

image

 

DisplayUpdateAirlineService.xml:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
name="com.sabre.redapp.example3.workflow.extension.service.DisplayUpdateAirlineFormService">
   <implementation
class="com.sabre.redapp.example3.workflow.extension.service.impl.DisplayUpdateAirlineFormServiceImpl"/>
   <service>
      <provide
interface="com.sabre.redapp.example3.workflow.extension.service.DisplayUpdateAirlineFormService"/>
   </service>
</scr:component> 

 

6.      Create Eclipse service which use data from user input. It will read data model class used for gathering data from user and then will modify LFS request by setting preferred carrier to airline code received from first data model.

a.)    Interface

 

image

 

ModifyAirlineService.java:

/
 * Service modifying host command to host request.
 */
public interface ModifyAirlineService
{
    /
     * Modifies host command
     * @param extPointCommand {@link FlowExtPointCommand} flow
     * @return {@link FlowExtPointCommand} flow
     */
    FlowExtPointCommand execute(FlowExtPointCommand extPointCommand);
}

 

b.)    Implementation

 

image

 

ModifyAirlineServiceImpl.java:

/
 * Service modifying airline in request.
 */
public class ModifyAirlineServiceImpl implements ModifyAirlineService
{
    /
     * {@inheritDoc}
     */
    @Override
    public FlowExtPointCommand execute(FlowExtPointCommand extPointCommand)
    {
        Optional <Airline> airlineOptional =
            fetchRequest(extPointCommand, Airline.class);

        if (airlineOptional.isPresent())
        {
           String iataCode = airlineOptional.get().getIataCode().trim();
            Optional <FlowExtPointRequestWrapper> rqWrapper = fetchRequestWrapper(extPointCommand,
RedAppAirShoppingRq.class); 

            if (rqWrapper.isPresent() && !iataCode.isEmpty())
            {
                RedAppAirShoppingRq airShoppingRq = (RedAppAirShoppingRq) rqWrapper.get().getRequest();
                airShoppingRq.getAdvancedOptions().getPreferredCarriers().clear();
                airShoppingRq.getAdvancedOptions().getPreferredCarriers().add(iataCode);
                rqWrapper.get().setOperation(FlowExtPointDataOperation._MODIFY_);
            }
        } 
        return extPointCommand;
    } 

    private <T> Optional <FlowExtPointRequestWrapper> fetchRequestWrapper(
        FlowExtPointCommand extPointCommand, Class <T> type)
    {
        return extPointCommand.getRequests().stream().filter(wrapper -> type.isInstance(wrapper.getRequest()))
            .findFirst();
    } 

    private <T> Optional <T> fetchRequest(FlowExtPointCommand extPointCommand, Class <T> type)
    {
        return extPointCommand.getRequests().stream().map(wrapper -> wrapper.getRequest())
            .filter(type::isInstance).map(type::cast).findFirst();
    }
}

 

d.)    Xml configuration file in the OSG-INF directory. This file must contain:

-        Unique name

-        Full path to class implementation

-        Full path to interface which will provide service functionalities.

 

image

 

ModifyAirlineService.xml:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
name="com.sabre.redapp.example3.workflow.extension.service.ModifyAirlineService">
       <implementation
class="com.sabre.redapp.example3.workflow.extension.service.impl.ModifyAirlineServiceImpl"/>
   <service>
      <provide interface="com.sabre.redapp.example3.workflow.extension.service.ModifyAirlineService"/>
   </service>
</scr:component>

    Create a service with the Gather PQR number from the RedApp model and pass it to the Container class.

a.) Interface. It must contain a method with one argument, which is a type of FlowExtPointCommand, and return the same FlowExtPointCommand type.

image

ShowPqrNumberService.java:

package com.sabre.redapp.example3.workflow.extension.service;

import com.sabre.stl.pos.srw.nextgen.flow.ext.v2.FlowExtPointCommand;

/**
 * The service extracts the pqr number and shows it to the user.
 */
public interface ShowPqrNumberService
{
    /**
     * Extract pqr number.
     *
     * @param extPointCommand {@link FlowExtPointCommand} flow
     * @return {@link FlowExtPointCommand} flow
     */
    FlowExtPointCommand execute(FlowExtPointCommand extPointCommand);
}

b.)    Implementation

image

ShowPqrNumberServiceImpl.java:

/**
 * The service extracts the pqr number in its response and shows it to the user.
 */
public class ShowPqrNumberServiceImpl implements ShowPqrNumberService
{

    @Override
    public FlowExtPointCommand execute(FlowExtPointCommand flowExtPointCommand)
    {
        Container container = new Container();
        container.setValue(flowExtPointCommand.getCommand());
        container.setExtpoint(flowExtPointCommand.getExtensionPointName());

        Optional <CommandMessageExchangeRs> commMsgExchangeRsOpt =
            fetchResponse(flowExtPointCommand, CommandMessageExchangeRs.class);

        if (commMsgExchangeRsOpt.isPresent())
        {
            CommandMessageExchangeRs commandMessageExchangeRs = commMsgExchangeRsOpt.get();
            container
                .setInfo("PQR Number: " + commandMessageExchangeRs.getData().getPqrNumber());
        }

        FlowExtPointResponse response = new FlowExtPointResponse();
        response.setStructure(container);

        FlowExtPointResponseWrapper responseWrapper = new FlowExtPointResponseWrapper();
        responseWrapper.setOperation(FlowExtPointDataOperation.ADD);
        responseWrapper.setResponse(response);
        flowExtPointCommand.getResponses().add(responseWrapper);

        return flowExtPointCommand;
    }
...

c.)    Service declaration file in the OSG-INF directory. This file must contain:

-        Unique name

-        Full path to class implementation

-        Full path to interface which will provide service functionalities.

image

ShowPqrNumberService.xml:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="com.sabre.redapp.example3.workflow.extension.service.ShowPqrNumberService">
   <implementation class="com.sabre.redapp.example3.workflow.extension.service.impl.ShowPqrNumberServiceImpl"/>
   <service>
      <provide interface="com.sabre.redapp.example3.workflow.extension.service.ShowPqrNumberService"/>
   </service>
</scr:component>

7.      Add services declaration files into MANIFEST.MF:

image

Service-Component: OSGI-INF/ModifyAirlineService.xml,
OSGI-INF/DisplayUpdateAirlineService.xml,
OSGI-INF/DisplayFlowExtPointService.xml,
OSGI-INF/ShowPqrNumberService.xml

 

In Web Module.

This Web Module is responsible for displaying popups to the user, gathering data and sending it back to back-end. In the Web Module:

 

  1. Create a typescript data model. In the mappings use the same namespace as defined in transformer properties. The data model added in the extension point can be accessed via the command flow object at path:

[d.Structure][o.ExtensionPoint_Summary][namespace from transformer.properties]

image  

AirlineModel.ts:

import AbstractModel = app.AbstractModel;
import EnhancedResponseData = app.common.data.dto.EnhancedResponseData;
import DataOptions = app.common.data.dto.DataOptions;
import \{Initial} from 'sabre-ngv-core/decorators/classes/Initial';
import AbstractModelOptions = app.AbstractModelOptions;
import CommandFlow = app.common.data.dto.CommandFlow;  

@Initial<DataOptions>({
    dataRoot:
'[d.Structure][o.ExtensionPoint_Summary][com_sabre_redapp_example_workflow_extension.Airline]'
})  

@Initial<AbstractModelOptions>({
    autoPropagateData: true,
    nonLazyMembers: [
        'iataCode'
    ]
})  

export class AirlineModel extends EnhancedResponseData {
    getIataCode() {
        return this.fromRoot().get('[com_sabre_redapp_example_workflow_extension.iataCode]').value();
    }  

    getCF() {
        return this.getCommandFlow();
    }
}

 

 

AirlineRequest.ts;

export interface AirlineRequest {'com_sabre_redapp_example_workflow_extension.Airline': [{
        'com_sabre_redapp_example_workflow_extension.iataCode': string,
    }];
}

 

AirlineRequestData.ts:

import RequestData = app.common.data.dto.request.RequestData;
import {AirlineRequest} from './AirlineRequest';  

export class AirlineRequestData extends RequestData<AirlineRequest> {      
    constructor(private iataCode: string) {
        super();
    }      
    getRequestStructure(): AirlineRequest {
        const iataCode = this.iataCode;
        return {
            'com_sabre_redapp_example_workflow_extension.Airline': [{
                'com_sabre_redapp_example_workflow_extension.iataCode': iataCode,
            }]
        };
    }
}

ContainerModel.ts:

import {EnhancedResponseData} from 'sabre-ngv-app/app/common/data/dto/EnhancedResponseData';
import {DataOptions} from 'sabre-ngv-app/app/common/data/dto/DataOptions';
import {Initial} from 'sabre-ngv-core/decorators/classes/Initial';
import {AbstractModelOptions} from 'sabre-ngv-app/app/AbstractModelOptions';

@Initial<DataOptions>({
    dataRoot: '[d.Structure][o.ExtensionPoint_Summary][com_sabre_redapp_example_workflow_extension.Container]'
})

@Initial<AbstractModelOptions>({
    autoPropagateData: true,
    nonLazyMembers: [
        'value',
        'extpoint',
        'info'
    ]
})
export class ContainerModel extends EnhancedResponseData {

    getTextCommand(): unknown {
        return this.fromRoot().get('[com_sabre_redapp_example_workflow_extension.value]').value();
    }

    getExtpoint(): unknown {
        return this.fromRoot().get('[com_sabre_redapp_example_workflow_extension.extpoint]').value();
    }

    getInfo(): unknown {
        return this.fromRoot().get('[com_sabre_redapp_example_workflow_extension.info]').value();
    }
}
  1. Create a view handler. It defines actions on a view. In this case, these will be the onClose and onSave actions. Additionally, the 'finalize' event has to be handled as well. It handles the case when the modal window is closed using the 'x' (close) icon in the top right corner.

image

 

AirlineView.ts:

export class AirlineView extends AbstractView<AirlineModel> {     
  constructor(options?: AbstractViewOptions) {
        super(options);
        if (options instanceof AirlineModel) {
            this.setModel(options);
        }
        this.on('save-action', this._onSaveAction);
        this.on('close-action', this._onCloseAction);
        this.on('finalize', this._onCloseAction);
    }  

    @Bound
    _onCloseAction(){
        let model = this.getModel();
        let commandFlow = model.getCF();
        commandFlow.getFlowControl().setFlowControlAction('SKIP');
        commandFlow.send();
        this.triggerOnEventBus('close-modal');
    }  

    @Bound
    _onSaveAction() {
        let iataCode = this.$el.find('#iataCode').val();
        let model = this.getModel();
        let commandFlow = model.getCF().addRequestDataObject(new AirlineRequestData(iataCode));
        commandFlow.send();
        this.triggerOnEventBus('close-modal');
    }
}

  ContainerView.ts:

import {AbstractView} from 'sabre-ngv-app/app/AbstractView';
import {AbstractViewOptions} from 'sabre-ngv-app/app/AbstractViewOptions';
import {getService} from '../Context';
import {I18nService} from 'sabre-ngv-app/app/services/impl/I18nService';

import {CssClass} from 'sabre-ngv-core/decorators/classes/view/CssClass';
import {Initial} from 'sabre-ngv-core/decorators/classes/Initial';
import {Template} from 'sabre-ngv-core/decorators/classes/view/Template';
import {ContainerModel} from '../models/ContainerModel';
import {Bound} from 'sabre-ngv-core/decorators/methods/Bound';

const i18nService: I18nService = getService(I18nService);

@CssClass('profile-details-view')
@Template('com-sabre-redapp-example3-workflow-extension-web-module:ContainerTemplate')
@Initial<AbstractViewOptions>({
    templateOptions: {
        helpers: {
            _t: i18nService.getScopedHelper('com_sabre_sdk_sample_workflow/translations')
        }
    }
})
export class ContainerView extends AbstractView<ContainerModel> {

    constructor(options?: AbstractViewOptions) {
        super(options);
        if (options instanceof ContainerModel) {
            this.setModel(options);
        }
        this.on('close-action', this._onCloseAction);
    }

    @Bound
    _onCloseAction() {
        let model = this.getModel();
        let commandFlow = model.getCommandFlow();
        commandFlow.getFlowControl().setFlowControlAction('SKIP');
        commandFlow.send();

        this.triggerOnEventBus('close-modal');
    }
}
  1. Create the view. It is an html template which will be displayed inside of the popup.

image

 

AirlineTemplate.html:

<div class="workflow-container-content">     
  <div id="workflow-container-content-body">
        <label class="control-label">Airline</label>
        <input type="text" id="iataCode" value="{{iataCode}}" />
  </div>
</div>

ContainerView.html:

<div class="workflow-container-content">
    <div id="workflow-container-content-body">
        Command: <label class="control-label" id="Command: ">{{command}}</label></br />
        Extension point: <label class="control-label" id="Extension point: ">{{extpoint}}</label></br />
        Info: <label class="control-label" id="Data: ">{{info}}</label>
    </div>
</div>

  11. Create a listener which will create a popup for gathering user input. It will register to a 'data-received' event, and when the received data has the required type it displays a popup, using the view as defined in the previous steps.

 

image

 

WorkFlowListener.ts:

export class WorkFlowListener extends AbstractObject {

    registerListener() {
        this.listenToEventBus('data-received', this.dataReceivedHandler);
    }

    dataReceivedHandler(data: Data): void {
        if (data instanceof AirlineModel) {
            getService(LayerService).showInModal(new AirlineView({ model: data }), {
                title: 'AirlineView',
                actions: [{
                    caption: 'Skip',
                    actionName: 'close',
                    type: 'secondary',
                    cssClass: 'seat-map-btn-close'
                } as any, {
                    caption: 'Submit',
                    actionName: 'save',
                    type: 'success',
                    cssClass: 'seat-map-btn-save btn btn-success'
                }, {
                    caption: 'Cancel',
                    actionName: 'cancel',
                    type: 'secondary',
                    cssClass: 'seat-map-btn-close'
                }]
            }, {
                    display: 'areaView'
                });
        } else if ( data instanceof ContainerModel ) {
            getService(LayerService).showInModal(new ContainerView({ model: data }), {
                title: 'Flow Extension Point',
                actions  : [{
                    caption   : 'Ok',
                    actionName: 'close',
                    type: 'success',
                    cssClass: 'seat-map-btn-save btn-success'
                } as any]
            }, {
                display: 'areaView'
            });
        }
    }
}
  1. In Main.ts register, as defined in the earlier steps, there should be the following things:

a.) A data model. Use the full path to access its data model from the command flow JSON object. Same as in data model definition.

b.)    view

c.)    listener

 

image

 

Main.ts:

export class Main extends Module {
    init(): void {
        super.init();
        const dto = getService(DtoService);

        new WorkFlowListener().registerListener();

        dto.registerDataModel(
            '[d.Structure][o.ExtensionPoint_Summary][com_sabre_redapp_example_workflow_extension.Airline]',
            AirlineModel
        );
        dto.registerDataView(AirlineModel, AirlineView);

        dto.registerDataModel(
            '[d.Structure][o.ExtensionPoint_Summary][com_sabre_redapp_example_workflow_extension.Container]',
            ContainerModel
        );
        dto.registerDataView(ContainerModel, ContainerView);

    }
}

13.   Compile webmodule. Put compiled module into web directory.

 

image

 

In plugin.xml

 

14.   Register services to extension points and register web module

 

image

 

a.) Extension points. In this case it is the LFS flow (dynamo.air.lowfareshopping) and the beforeShoppingRequest extension points. One service is registered as front-end(manual) for providing data model for front-end and other is back-end(auto) type.

 

	<extension
         point="com.sabre.edge.dynamo.flow.flowextpoint.registry">
      <flowExtensionPoint
            callbackService="com.sabre.redapp.example3.workflow.extension.service.DisplayUpdateAirlineFormService:execute"
            flowId="dynamo.air.lowfareshopping"
            extensionPointId="beforeShoppingRequest"
			type="manual">
      </flowExtensionPoint>
      <flowExtensionPoint
            callbackService="com.sabre.redapp.example3.workflow.extension.service.ModifyAirlineService:execute"
            flowId="dynamo.air.lowfareshopping"
            extensionPointId="beforeShoppingRequest">
      </flowExtensionPoint>
   </extension>

   <extension
         point="com.sabre.edge.dynamo.flow.flowextpoint.registry">
      <flowExtensionPoint
            callbackService="com.sabre.redapp.example3.workflow.extension.service.ShowPqrNumberService:execute"
            flowId="dynamo.exchange.confirmation"
            extensionPointId="afterExchangeConfirmation"
            type="manual">
      </flowExtensionPoint>
   </extension>

 

 

 

b.)    Webmodule.

   <extension point="com.sabre.edge.dynamo.web.module">       
       <modules>
        <module id="com-sabre-redapp-example-workflow-extension"/>
       </modules>
    </extension>