Chapter 6. Trip Reservation Example

Trip Reservation is about a travel agency that offers its services via the web. Travelers pick their choice of flight, hotel and/or rental car. The agency receives orders from its customers and gives invoices in return.

Invoices contain a trip locator. Travelers keep this locator for further reference. At any time before the trip begins, travelers are able to check the details of their trip. Should a traveler want to cancel, the travel agency assesses a cancellation fee and reimburses the remaining portion of the amount in the invoice.

The figure below represents the top level activities of the trip reservation process.

Trip reservation process

Figure 6.1. Trip reservation process

6.1. Define the BPEL process

6.1.1. Create the BPEL document

This process has two partnerships. The first, traveler is the immediately noticeable link with the customers of the agency. The second is more of an implementation detail and consists of a service that generates the trip locators. In fact, this process reuses the ticket issuer introduced in the ATM example.

The correlation set trip involves activities that exchange the trip locator with external entities. The trip locator helps distinguish reservations from each other.

<process name="TripReservation" targetNamespace="http://jbpm.org/examples/trip"
  xmlns:tns="http://jbpm.org/examples/trip" xmlns:tic="http://jbpm.org/examples/ticket"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:bpel="http://schemas.xmlsoap.org/ws/2003/03/business-process/"
  xmlns="http://schemas.xmlsoap.org/ws/2003/03/business-process/">

  <partnerLinks>
    <partnerLink name="traveler" partnerLinkType="tns:Traveler-Agent" 
      myRole="Agent" />
    <partnerLink name="ticket" partnerLinkType="tns:Agent-Ticket" 
      partnerRole="TicketIssuer" />
  </partnerLinks>

  <variables>
    <variable name="purchaseRequest" messageType="tns:purchaseRequest" />
    <variable name="cost" type="xsd:double" />
    <variable name="items" type="tns:ItemSet" />
    <variable name="cancelRequest" messageType="tns:cancelRequest" />
    <variable name="cancelResponse" messageType="tns:cancelResponse" />
    <variable name="detailRequest" messageType="tns:detailRequest" />
    <variable name="detailResponse" messageType="tns:detailResponse" />
    <variable name="dateReached" type="xsd:boolean" />
  </variables>

  <correlationSets>
    <correlationSet name="trip" properties="tns:tripLocator" />
  </correlationSets>

  <faultHandlers>
    ...
  </faultHandlers>
  
  <sequence name="Main">
    ...
  </sequence>

</process>

The trip purchase section of the process looks like this:

Trip purchase unit

Figure 6.2. Trip purchase unit

and behaves like this: after deciding on the desired trip options, the traveler sends a purchase order which goes to the purchaseRequest variable. The cost of the trip is maintained in variable cost and is initially set to zero. The process examines the order and charges the flight, hotel and rental car if requested. In parallel, the process contacts the ticket service and obtains a trip locator. Lastly, an invoice is prepared and sent back to the traveler. The invoice contains the trip locator for the customer's further reference.

<scope name="TripPurchase">

  <variables>
    <variable name="purchaseResponse" messageType="tns:purchaseResponse" />
    <variable name="ticketRequest" messageType="tic:ticketRequest" />
    <variable name="ticketMessage" messageType="tic:ticketMessage" />
  </variables>

  <sequence name="PurchaseTrip">

    <receive name="ReceiveTripOrder" operation="purchaseTrip" partnerLink="traveler"
      portType="tns:TravelAgent" variable="purchaseRequest" createInstance="yes" />

    <flow>

      <sequence name="EvaluateCost">

        <assign name="InitializeCost">
          <copy>
            <from expression="0" />
            <to variable="cost" />
          </copy>
          <copy>
            <from variable="purchaseRequest" part="order" query="/tns:order/items" />
            <to variable="items" />
          </copy>
        </assign>

        <switch name="FlightDecision">

          <case condition="bpel:getVariableData('items')/flight
            and string(bpel:getVariableData('items')/flight/@xsi:nil) != '1'">

            <scope name="FlightReservation">

              <compensationHandler>
                <assign name="ReimburseFlight">
                  <copy>
                    <from expression="bpel:getVariableData('cost') - 300 + 100" />
                    <to variable="cost" />
                  </copy>
                </assign>
              </compensationHandler>

              <assign name="ChargeFlight">
                <copy>
                  <from expression="bpel:getVariableData('cost') + 300" />
                  <to variable="cost" />
                </copy>
              </assign>

            </scope>

          </case>

        </switch>

        <switch name="HotelDecision">

          <case condition="bpel:getVariableData('items')/hotel
            and string(bpel:getVariableData('items')/hotel/@xsi:nil) != '1'">

            <scope name="HotelReservation">

              <compensationHandler>
                <assign name="ReimburseHotel">
                  <copy>
                    <from expression="bpel:getVariableData('cost') - 100 + 25" />
                    <to variable="cost" />
                  </copy>
                </assign>
              </compensationHandler>

              <assign name="ChargeHotel">
                <copy>
                  <from expression="bpel:getVariableData('cost') + 100" />
                  <to variable="cost" />
                </copy>
              </assign>

            </scope>

          </case>

        </switch>

        <switch name="CarDecision">

          <case condition="bpel:getVariableData('items')/rentalCar
            and string(bpel:getVariableData('items')/rentalCar/@xsi:nil) != '1'">

            <scope name="CarReservation">

              <compensationHandler>
                <assign name="ReimburseCar">
                  <copy>
                    <from expression="bpel:getVariableData('cost') - 50 + 5" />
                    <to variable="cost" />
                  </copy>
                </assign>
              </compensationHandler>

              <assign name="ChargeCar">
                <copy>
                  <from expression="bpel:getVariableData('cost') + 50" />
                  <to variable="cost" />
                </copy>
              </assign>

            </scope>

          </case>

        </switch>

      </sequence>

      <invoke name="CreateTicket" operation="createTicket" partnerLink="ticket"
        portType="tic:TicketIssuer" inputVariable="ticketRequest"
        outputVariable="ticketMessage">
        <correlations>
          <correlation set="trip" initiate="yes" pattern="in" />
        </correlations>
      </invoke>

    </flow>

    <assign name="PrepareInvoice">
      <copy>
        <from variable="ticketMessage" part="ticketNo" />
        <to variable="purchaseResponse" part="invoice" query="/tns:invoice/@locator" />
      </copy>
      <copy>
        <from variable="cost" />
        <to variable="purchaseResponse" part="invoice" query="/tns:invoice/@cost" />
      </copy>
    </assign>

    <reply name="SendInvoice" operation="purchaseTrip" partnerLink="traveler"
      portType="tns:TravelAgent" variable="purchaseResponse">
      <correlations>
        <correlation set="trip" />
      </correlations>
    </reply>

  </sequence>

</scope>

After the invoice is sent, the process expects one of the following three external events to occur:

  1. the traveler reviews the trip details

  2. the traveler cancels the trip

  3. the trip date arrives

These events appear as branches in the next model.

Loop before trip date

Figure 6.3. Loop before trip date

Travelers can review the trip details as many times as they want before the trip date. For this reason, the event listening structure appears inside a loop. Arriving at the trip date just breaks the loop.

<assign name="SetDateNotReached">
  <copy>
    <from expression="false()" />
    <to variable="dateReached" />
  </copy>
</assign>

<while name="PredateLoop" condition="bpel:getVariableData('dateReached') = 'false'">

  <pick name="PredateMenu">

    <onMessage operation="getTripDetail" partnerLink="traveler" portType="tns:TravelAgent"
      variable="detailRequest">

      <correlations>
        <correlation set="trip" />
      </correlations>

      <sequence name="DetailTrip">

        <assign name="PrepareTripDetail">
          <copy>
            <from variable="purchaseRequest" part="order" query="/tns:order/items" />
            <to variable="detailResponse" part="detail" query="/tns:detail/items" />
          </copy>
          <copy>
            <from variable="cost" />
            <to variable="detailResponse" part="detail" query="/tns:detail/@cost" />
          </copy>
        </assign>

        <reply name="SendTripDetail" operation="getTripDetail" partnerLink="traveler"
          portType="tns:TravelAgent" variable="detailResponse" />

      </sequence>

    </onMessage>

    <onMessage operation="cancelTrip" partnerLink="traveler" portType="tns:TravelAgent"
      variable="cancelRequest">

      <correlations>
        <correlation set="trip" />
      </correlations>

      <throw name="CancelTrip" faultName="tns:cancelation" />

    </onMessage>

    <onAlarm until="bpel:getVariableData('purchaseRequest', 'order', '/tns:order/date')">

      <assign name="SetDateReached">
        <copy>
          <from expression="true()" />
          <to variable="dateReached" />
        </copy>
      </assign>

    </onAlarm>

  </pick>

</while>

This example takes a distinct approach to handle the cancelation scenario. Instead of branching to determine what options were ordered, this process throws a fault. A global fault handler compensates the trip purchase scope. Compensation handlers (installed only for inner scopes that got activated) reimburse charges and assess applicable fees. In the end, a bill is sent back to report the forfeited amount.

<catch faultName="tns:cancelation">

  <sequence name="Cancel">

    <compensate name="UndoPurchase" scope="tripPurchase" />

    <assign name="PreparePenalty">
      <copy>
        <from variable="cost" />
        <to variable="cancelResponse" part="penalty" query="/tns:penalty/@fee" />
      </copy>
    </assign>

    <reply name="SendPenalty" operation="cancelTrip" partnerLink="traveler"
      portType="tns:TravelAgent" variable="cancelResponse" />

  </sequence>

</catch>

6.1.2. Create/obtain the WSDL interface documents

The WSDL file trip.wsdl describes the travel agency service. Apart from schema types, messages and operations, the document characterizes the relationship between (a) the traveler and the agency (Traveler-Agent ) and (b) the agency and the trip locator issuer (Agent-Ticket ). The WSDL definition also introduces the trip locator as a property and maps it to fields within the invoice, cancelation and query messages.

<definitions name="trip" targetNamespace="http://jbpm.org/examples/trip"
  xmlns:tns="http://jbpm.org/examples/trip" xmlns:tic="http://jbpm.org/examples/ticket"
  xmlns:plt="http://schemas.xmlsoap.org/ws/2003/05/partner-link/"
  xmlns:bpel="http://schemas.xmlsoap.org/ws/2003/03/business-process/"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.xmlsoap.org/wsdl/">

  <import namespace="http://jbpm.org/examples/ticket" location="ticket.wsdl" />

  <types>

    <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
      targetNamespace="http://jbpm.org/examples/trip">

      <xsd:complexType name="Flight">
        <xsd:attribute use="required" name="airline" type="xsd:string" />
        <xsd:attribute use="required" name="number" type="xsd:int" />
      </xsd:complexType>

      <xsd:complexType name="Hotel">
        <xsd:attribute use="required" name="name" type="xsd:string" />
      </xsd:complexType>

      <xsd:complexType name="RentalCar">
        <xsd:attribute use="required" name="company" type="xsd:string" />
      </xsd:complexType>

      <xsd:complexType name="ItemSet">
        <xsd:sequence>
          <xsd:element name="flight" minOccurs="0" type="tns:Flight" />
          <xsd:element name="hotel" minOccurs="0" type="tns:Hotel" />
          <xsd:element name="rentalCar" minOccurs="0" type="tns:RentalCar" />
        </xsd:sequence>
      </xsd:complexType>

      <xsd:element name="order">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="items" type="tns:ItemSet" />
            <xsd:element name="date" type="xsd:dateTime" />
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>

      <xsd:element name="invoice">
        <xsd:complexType>
          <xsd:sequence />
          <xsd:attribute name="locator" type="xsd:int" use="required" />
          <xsd:attribute name="cost" type="xsd:double" use="required" />
        </xsd:complexType>
      </xsd:element>

      <xsd:element name="cancelation">
        <xsd:complexType>
          <xsd:sequence />
          <xsd:attribute name="locator" type="xsd:int" use="required" />
        </xsd:complexType>
      </xsd:element>

      <xsd:element name="penalty">
        <xsd:complexType>
          <xsd:sequence />
          <xsd:attribute name="fee" type="xsd:double" use="required" />
        </xsd:complexType>
      </xsd:element>

      <xsd:element name="query">
        <xsd:complexType>
          <xsd:sequence />
          <xsd:attribute name="locator" type="xsd:int" use="required" />
        </xsd:complexType>
      </xsd:element>

      <xsd:element name="detail">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="items" type="tns:ItemSet" />
          </xsd:sequence>
          <xsd:attribute name="cost" type="xsd:double" use="required" />
        </xsd:complexType>
      </xsd:element>

    </xsd:schema>

  </types>

  <message name="purchaseRequest">
    <part name="order" element="tns:order" />
  </message>

  <message name="purchaseResponse">
    <part name="invoice" element="tns:invoice" />
  </message>

  <message name="cancelResponse">
    <part name="penalty" element="tns:penalty" />
  </message>

  <message name="detailResponse">
    <part name="detail" element="tns:detail" />
  </message>

  <message name="detailRequest">
    <part name="query" element="tns:query" />
  </message>

  <message name="cancelRequest">
    <part name="cancelation" element="tns:cancelation" />
  </message>

  <portType name="TravelAgent">
    <operation name="purchaseTrip">
      <input message="tns:purchaseRequest" />
      <output message="tns:purchaseResponse" />
    </operation>
    <operation name="cancelTrip">
      <input message="tns:cancelRequest" />
      <output message="tns:cancelResponse" />
    </operation>
    <operation name="getTripDetail">
      <input message="tns:detailRequest" />
      <output message="tns:detailResponse" />
    </operation>
  </portType>

  <plt:partnerLinkType name="Traveler-Agent">
    <plt:role name="Agent">
      <plt:portType name="tns:TravelAgent" />
    </plt:role>
  </plt:partnerLinkType>

  <plt:partnerLinkType name="Agent-Ticket">
    <plt:role name="TicketIssuer">
      <plt:portType name="tic:TicketIssuer" />
    </plt:role>
  </plt:partnerLinkType>

  <bpel:property name="tripLocator" type="xsd:int" />

  <bpel:propertyAlias propertyName="tns:tripLocator" messageType="tns:purchaseResponse"
    part="invoice" query="/tns:invoice/@locator" />

  <bpel:propertyAlias propertyName="tns:tripLocator" messageType="tns:cancelRequest"
    part="cancelation" query="/tns:cancelation/@locator" />

  <bpel:propertyAlias propertyName="tns:tripLocator" messageType="tns:detailRequest" 
    part="query" query="/tns:query/@locator" />

  <bpel:propertyAlias propertyName="tns:tripLocator" messageType="tic:ticketMessage"
    part="ticketNo" />

</definitions>

Starting from version 1.1.GA, jBPM BPEL parses incoming messages and formats outgoing messages using full WSDL binding information. This means support for the document style and enhancements for the RPC style.

Unlike other examples, the TravelAgent port type refers only to wsdl:parts that have been defined using the element attribute. This characterizes the document style.

There is another document, ticket.wsdl, that describes the ticket issuer interface. Please jump to the Create/obtain the WSDL interface documents section of the ATM example if you want to examine this document.

6.1.3. Deploy the process definition

The master WSDL document trip.wsdl contains most WSDL definitions. It imports ticket.wsdl to access the definitions specific to the ticket issuer.

Deploy the process definition to the jBPM database calling:

ant deploy.process

The deploy-process target builds the process archive trip.zip and submits it to the deployment servlet. In a typical deployment the server prints these messages:

21:10:23,203 INFO  [DeploymentServlet] deployed process definition: TripReservation
21:10:25,250 INFO  [WebModuleBuilder] packaged web module: trip.war
21:10:25,250 INFO  [DeploymentServlet] deployed web module: trip.war
21:10:27,359 INFO  [DefaultEndpointRegistry] register: jboss.ws:context=trip, «
endpoint=AgentServlet
21:10:27,437 INFO  [TomcatDeployer] deploy, ctxPath=/trip, warUrl=.../tmp41529trip-exp.war/
21:10:28,250 INFO  [IntegrationConfigurator] message reception enabled for process: «
TripReservation
21:10:28,546 INFO  [WSDLFilePublisher] WSDL published to: .../trip-service.wsdl

Back in the description of the process, we mentioned a partner service that generates unique trip locators. Make sure you deploy that service before any reservation is placed, or the process will malfunction and your virtual travelers will be very angry. Change to the ticket directory and trigger the following target.

ant deploy.webservice

The next few lines confirm a successful deployment.

18:12:44,421 INFO  [DefaultEndpointRegistry] register: jboss.ws:context=ticket, «
endpoint=ticketIssuerServlet
18:12:44,437 INFO  [TomcatDeployer] deploy, ctxPath=/ticket, warUrl=...
18:12:44,843 INFO  [WSDLFilePublisher] WSDL published to: .../ticket-impl.wsdl

The JBossWS service endpoints page should be showing a picture similar to the next one. Of course, the host address and port shown may vary, depending on your server bind address.

Endpoints page

Figure 6.4. Endpoints page

Note

It is worth pointing BPEL endpoints are intermixed with Java and EJB endpoints in the list. For all intents and purposes, BPEL endpoints are no different.

Deploying a partner service is not enough for the jBPM BPEL to know of its existence. Its WSDL description must be registered in the service catalog. The target below is provided for this purpose.

ant register.partners

The web console allows you to browse the catalog. You can register new partner services manually as well.

Service catalog

Figure 6.5. Service catalog

6.2. Build the WSEE application client

6.2.1. Application client deployment descriptor

Reference your full WSDL description and Java mapping artifacts from the application-client.xml descriptor.

<application-client version="1.4" xmlns="http://java.sun.com/xml/ns/j2ee">

  <display-name>Trip Reservation Client</display-name>

  <service-ref>

    <!-- JNDI name of service interface in client environment context -->
    <service-ref-name>service/Trip</service-ref-name>
    <!-- service interface -->
    <service-interface>
      org.jbpm.bpel.tutorial.trip.TripReservationService
    </service-interface>
    <!-- published WSDL document -->
    <wsdl-file>META-INF/wsdl/trip-service.wsdl</wsdl-file>
    <!-- Java<->XML mapping file -->
    <jaxrpc-mapping-file>META-INF/trip-mapping.xml</jaxrpc-mapping-file>

  </service-ref>

</application-client>

6.2.2. Environment context

Allocate a JNDI name for the client environment context in jboss-client.xml .

<jboss-client>
  <jndi-name>jbpmbpel-client</jndi-name>
</jboss-client>

Tip

The jndi-name above is shared among all examples. You can share a single JNDI name among multiple application clients to keep them organized and reduce the number of top-level entries in the global JNDI context of the server. Just make sure you give different service-ref-names to each client in the respective application-client.xml file.

6.3. Test the process

To ensure the process works as expected, the test case TravelAgentTest is provided.

6.3.1. Remote web service access

The test setup code looks up the service instance tripService. This object is a factory from which clients get service endpoint proxies. Sample flight, hotel and car entities are also initialized.

private TravelAgent agent;

private Flight flight = new Flight();
private Hotel hotel = new Hotel();
private RentalCar car = new RentalCar();
private Calendar tripDate = Calendar.getInstance();

protected void setUp() throws Exception {
  /*
   * "service/Trip" is the JNDI name of the service interface instance relative to
   * the client environment context. This name matches the <service-ref-name> in 
   * application-client.xml
   */
  InitialContext iniCtx = new InitialContext();
  TripReservationService tripService = (TripReservationService) iniCtx.lookup(
    "java:comp/env/service/Trip");
  agent = tripService.getAgentPort();

  flight.setAirline("AM");
  flight.setNumber(637);
  hotel.setName("Maria Isabel");
  car.setCompany("Alamo");
  tripDate.add(Calendar.SECOND, 10);
}

There are three test scenarios.

  1. testPurchaseTrip: create a trip reservation that includes a flight and a hotel. Submit the reservation. Ensure the cost in the invoice matches the price of the selected items.

    public void testPurchaseTrip() throws RemoteException {
      ItemSet items = new ItemSet();
      items.setFlight(flight); // cost: 300
      items.setHotel(hotel); // cost: 100
    
      Order order = new Order();
      order.setDate(tripDate);
      order.setItems(items);
    
      Invoice invoice = agent.purchaseTrip(order);
    
      assertEquals(300 + 100, invoice.getCost(), 0);
    }
  2. testGetTripDetails: reserve a trip, including all offered items. Use the trip locator in the returned invoice to query about the details. Verify the details match the original order.

    public void testGetTripDetails() throws RemoteException {
      ItemSet items = new ItemSet();
      items.setFlight(flight);
      items.setHotel(hotel);
      items.setRentalCar(car);
    
      Order order = new Order();
      order.setDate(tripDate);
      order.setItems(items);
    
      Invoice invoice = agent.purchaseTrip(order);
    
      Query query = new Query();
      query.setLocator(invoice.getLocator());
    
      Detail detail = agent.getTripDetail(query);
      items = detail.getItems();
    
      assertEquals(flight.getAirline(), items.getFlight().getAirline());
      assertEquals(flight.getNumber(), items.getFlight().getNumber());
      assertEquals(hotel.getName(), items.getHotel().getName());
      assertEquals(car.getCompany(), items.getRentalCar().getCompany());
      assertEquals(invoice.getCost(), detail.getCost(), 0);
    }
  3. testCancelTrip: make a reservation for a flight and a rental car. Cancel the trip using the locator in the invoice. Check the penalty corresponds to the ordered items.

    public void testCancelTrip() throws Exception {
      ItemSet items = new ItemSet();
      items.setFlight(flight); // fee: 100
      items.setRentalCar(car); // fee: 5
    
      Order order = new Order();
      order.setDate(tripDate);
      order.setItems(items);
    
      Invoice invoice = agent.purchaseTrip(order);
    
      Cancelation reference = new Cancelation();
      reference.setLocator(invoice.getLocator());
    
      Penalty penalty = agent.cancelTrip(reference);
    
      assertEquals(100 + 5, penalty.getFee(), 0);
    }

Note

The process instances created to serve the above requests will end automatically, on or shortly after the stated trip date and time.

6.3.2. Client JNDI properties

The JNDI properties are exactly the same for all examples. In particular, the j2ee.clientName property does not change because all examples share the <jndi-name> in their respective jboss-client.xml descriptors. Refer to the Client JNDI properties section in the first example for a listing of the properties.

6.3.3. Test execution

To execute the JUnit test, call:

ant test

The results you should expect follow.

test:
    [junit] Running org.jbpm.bpel.tutorial.trip.TravelAgentTest
    [junit] Tests run: 3, Failures: 0, Errors: 0, Time elapsed: 4.875 sec