Flux’s RestAction is the go-to construct for users orchestrating Flux workflows that involve web service integration. Be it HTTP-based services or SOAP-based services that implement HTTP binding, RestAction comes very handy for developers. Flux has a WebServiceAction which talks SOAP exclusively for services that implement WS-* such as Addressing, Security etc. RestAction has neat XPath integration, which allows users to navigate the response using XPath query and bind the result to domain model that can be seamlessly used in your workflow context.

In this post, let us look at a simple Weather Web Service which supports both SOAP and HTTP binding. I will be using Eric’s simple test framework to demonstrate the use of RestAction and WebServiceAction in your Flux workflow.

Let us see how to invoke the Weather web service using the traditional WebServiceAction that uses the WSDL. We pass the zip code as a parameter to GetCityForecastByZIP operation and the response is handled using a JavaAction. In this case the response is bound to ForecastReturn complex type.

package flux.test;

import com.cdyne.ws.weatherws.Forecast;
import com.cdyne.ws.weatherws.ForecastReturn;
import flux.*;
import org.junit.Test;

import java.net.URL;
import java.util.Properties;

public class WeatherWebServicesTest extends AbstractFluxTest {

 @Test
 public void testWebServiceAction() throws Exception {
   log.info("[START] FluxTest.testWebServiceAction");
   String namespace = "/FluxFlowChart";

   FlowChart flowChart = engineHelper.makeFlowChart(namespace);

   WebServiceAction webServiceAction = flowChart.makeWebService("GET Weather Info");

   // Specify the WSDL the web service action is to use.
   webServiceAction.setWsdl(new URL("http://wsf.cdyne.com/WeatherWS/Weather.asmx?WSDL"));

   // Specify the signature of the method to call.
   webServiceAction.setListenerSignature("GetCityForecastByZIP(String)");

   // Specify the arguments to pass to the method.
   webServiceAction.setArgument(0, "59102");

   JavaAction javaAction = flowChart.makeJavaAction("Java Action");
   javaAction.setListener(WebServiceActionListener.class);

   webServiceAction.addFlow(javaAction);

   engine.put(flowChart);

   waitForRuns(namespace, 1, 5, 30);

   log.info("[END] FluxTest.testWebServiceAction");
 }
}

The ActionListener implementation that processes the ForecastReturn from WebServiceAction.

Let us invoke the same operation using RestAction, which uses the HTTP binding. Here we set the query parameter “ZIP” and bind the response to ForecastReturn POJO (generated using: xjc -wsdl http://wsf.cdyne.com/WeatherWS/Weather.asmx?wsdl).

  @Test
  public void testRestActionGET() throws Exception {
    log.info("[START] FluxTest.testRestActionGET");
    String namespace = "/FluxFlowChart";

    FlowChart flowChart = engineHelper.makeFlowChart(namespace);

    RestAction restAction = flowChart.makeRestAction("GET Weather Info");

    // Specify the URL the rest action is to use.
    restAction.setBaseUrl(new URL("http://wsf.cdyne.com/WeatherWS/Weather.asmx/GetCityForecastByZIP"));

    // Specify the Action type.
    restAction.setActionType(RestActionType.GET);

    // Specify the Response type.
    restAction.setResponseType(ForecastReturn.class);

    // Specify the query parameters to be sent as part of the request.
    Properties queryParams = new Properties();
    queryParams.put("ZIP", "59102");

    restAction.setQueryParameters(queryParams);

    JavaAction javaAction = flowChart.makeJavaAction("Java Action");
    javaAction.setListener(RestActionListener.class);

    restAction.addFlow(javaAction);

    engine.put(flowChart);

    waitForRuns(namespace, 1, 5, 30);

    log.info("[END] FluxTest.testRestActionGET");
  }

In this example, we are performing an XPath query that returns an array of Forecast elements.

  @Test
  public void testRestActionXPathArray() throws Exception {
    log.info("[START] FluxTest.testRestActionXPathArray");
    String namespace = "/FluxFlowChart";

    FlowChart flowChart = engineHelper.makeFlowChart(namespace);

    RestAction restAction = flowChart.makeRestAction("GET Weather Info");

    // Specify the URL the rest action is to use.
    restAction.setBaseUrl(new URL("http://wsf.cdyne.com/WeatherWS/Weather.asmx/GetCityForecastByZIP"));

    // Specify the Action type.
    restAction.setActionType(RestActionType.GET);

    // Specify the query parameters to be sent as part of the request.
    Properties queryParams = new Properties();
    queryParams.put("ZIP", "59102");

    restAction.setQueryParameters(queryParams);

    // Specify the XML namespace, if one defined
    Properties namespaces = new Properties();
    namespaces.put("ws", "http://ws.cdyne.com/WeatherWS/");
    restAction.setNamespaces(namespaces);

    // Specify the XPath expression using the defined namespace
    restAction.setXpathExpression("/ws:ForecastReturn/ws:ForecastResult/ws:Forecast");// this xpath returns an array of Forecast elements

    // Specify the XPath response type.
    restAction.setXpathResponseType(Forecast[].class);

    JavaAction javaAction = flowChart.makeJavaAction("Java Action");
    javaAction.setListener(RestActionListener.class);

    restAction.addFlow(javaAction);

    engine.put(flowChart);

    waitForRuns(namespace, 1, 5, 30);

    log.info("[END] FluxTest.testRestActionXPathArray");
  }

In this example, we are performing an XPath query that returns the first Forecast element from the array.

  @Test
  public void testRestActionXPathPOJO() throws Exception {
    log.info("[START] FluxTest.testRestActionXPathPOJO");
    String namespace = "/FluxFlowChart";

    FlowChart flowChart = engineHelper.makeFlowChart(namespace);

    RestAction restAction = flowChart.makeRestAction("GET Weather Info");

    // Specify the URL the rest action is to use.
    restAction.setBaseUrl(new URL("http://wsf.cdyne.com/WeatherWS/Weather.asmx/GetCityForecastByZIP"));

    // Specify the Action type.
    restAction.setActionType(RestActionType.GET);

    // Specify the query parameters to be sent as part of the request.
    Properties queryParams = new Properties();
    queryParams.put("ZIP", "59102");

    restAction.setQueryParameters(queryParams);

    // Specify the XML namespace, if one defined
    Properties namespaces = new Properties();
    namespaces.put("ws", "http://ws.cdyne.com/WeatherWS/");
    restAction.setNamespaces(namespaces);

    // Specify the XPath expression using the defined namespace
    restAction.setXpathExpression("/ws:ForecastReturn/ws:ForecastResult/ws:Forecast[1]");// this xpath returns the first element from the list

    // Specify the XPath response type.
    restAction.setXpathResponseType(Forecast.class);

    JavaAction javaAction = flowChart.makeJavaAction("Java Action");
    javaAction.setListener(RestActionListener.class);

    restAction.addFlow(javaAction);

    engine.put(flowChart);

    waitForRuns(namespace, 1, 5, 30);

    log.info("[END] FluxTest.testRestActionXPathPOJO");
  }

In this example, we are performing an XPath query that returns an attribute of ForecastReturn element.

  @Test
  public void testRestActionXPathAttribute() throws Exception {
    log.info("[START] FluxTest.testRestActionXPathAttribute");
    String namespace = "/FluxFlowChart";

    FlowChart flowChart = engineHelper.makeFlowChart(namespace);

    RestAction restAction = flowChart.makeRestAction("GET Weather Info");

    // Specify the URL the rest action is to use.
    restAction.setBaseUrl(new URL("http://wsf.cdyne.com/WeatherWS/Weather.asmx/GetCityForecastByZIP"));

    // Specify the Action type.
    restAction.setActionType(RestActionType.GET);

    // Specify the query parameters to be sent as part of the request.
    Properties queryParams = new Properties();
    queryParams.put("ZIP", "59102");

    restAction.setQueryParameters(queryParams);

    // Specify the XML namespace, if one defined
    Properties namespaces = new Properties();
    namespaces.put("ws", "http://ws.cdyne.com/WeatherWS/");
    restAction.setNamespaces(namespaces);

    // Specify the XPath expression using the defined namespace
    restAction.setXpathExpression("/ws:ForecastReturn/ws:City/text()");// this xpath returns the name of the city

    // Specify the XPath response type.
    restAction.setXpathResponseType(String.class);

    JavaAction javaAction = flowChart.makeJavaAction("Java Action");
    javaAction.setListener(RestActionListener.class);

    restAction.addFlow(javaAction);

    engine.put(flowChart);

    waitForRuns(namespace, 1, 5, 30);

    log.info("[END] FluxTest.testRestActionXPathAttribute");
  }

The ActionListener implementation that processes various results from the RestAction is shown below.

Here is a screenshot of the test output from Intellij.

You need to toss in the following jars from Flux distribution:

flux.jar
lib/restaction.jar
lib/webserviceaction.jar
lib/commons-logging-1.1.1.jar
examples/software_developers/web_services/lib/weatherws-generated.jar

Test dependencies include:

log4j-1.2.15.jar
junit-4.9.jar


Download the latest Flux 7.11 version here and check it out. The samples are available in flux-goodies bitbucket repository. You can also find more samples ($FLUX_HOME/examples/software_developers/web_services and $FLUX_HOME/examples/software_developers/rest_services) in the latest Flux 7.11.2 distribution. Enjoy!

Possibly Related Posts:


There are two features that are very cool in the recent CXF 2.3 release that deserves its mention in the release notes/features document as they prove to be quite useful and powerful in certain use cases. Advanced search capabilities and Atom Logging features make CXF a compelling choice for developers looking for this support in JAX-RS frameworks. I first read about these features when Sergey introduced their availability in 2.3 snapshots.

In this post, I am going to explore FIQL based advanced search query support in CXF. I have experimented Atom logger before and it deserves a separate blog post and when used with FIQL, they are more powerful.

Introduction to FIQL Expressions in CXF

The Feed Item Query Language (FIQL, pronounced “fickle”) is a draft specification by Mark Nottingham that proposes URL-friendly syntax for expressing filters that operate on the feed entries. An FIQL expression is composed of one or more constraints, related to each other with Boolean operators. FIQL expressions yield Boolean values: True or False.

Let us look at each of the operators defined in the draft spec with an example FIQL expression and the equivalent SQL query produced by CXF.

Two boolean operators:
“;” is the Boolean AND operator; it yields True for a particular entry if both operands evaluate to True, otherwise False.

/sakila/searchActors?_s=firstname==PENELOPE;lastname==GUINESS
SELECT * FROM actor WHERE lastname = 'GUINESS' AND firstname = 'PENELOPE'

“,” is the Boolean OR operator; it yields True if either operand evaluates to True, otherwise False.

/sakila/searchActors?_s=lastname==MONROE,lastname==GUINESS
SELECT * FROM actor WHERE (lastname = 'MONROE') OR (lastname = 'GUINESS')

Two comparison operators are applicable to simple text comparisons:
“==” yields True if the string value (as per XPath) of any selected node matches the argument; otherwise False.

/sakila/searchActors?_s=lastname==MONROE
SELECT * FROM actor WHERE lastname = 'MONROE'

“!=” yields True if the string value of every selected node does not match the argument; otherwise False.

/sakila/searchActors?_s=lastname!=MONROE
SELECT * FROM actor WHERE lastname <> 'MONROE'

If the argument string begins or ends with an asterisk character (“*”), it acts as a wild card, matching any characters preceding or following (respectively) that position.

/sakila/searchActors?_s=lastname==PEN*
SELECT * FROM actor WHERE firstname LIKE 'PEN%'

[Note: The spec mentions that text comparisons should allow case insensitive textual content. CXF text comparison is case sensitive.]

Four operators are relevant to date comparisons:
“==” yields True if the point in time specified in the argument matches that indicated by the string-value of the selected node; otherwise False.

/sakila/searchRentals?_s=returndate==2005-08-31T20:00:25.000%2B06:00
SELECT * FROM rental WHERE returndate = 'Wed Aug 31 08:00:25 MDT 2005'

[Note: The database contains the following timestamp for the returndate ‘2005-08-31T20:00:25-06:00’, but when I query this using the above FIQL expression, it returned no matching entry. I tried another similar expression with no success: /sakila/searchRentals?_s=returndate==2005-08-31T20:00:25Z]

“!=” yields True if the point in time specified in the argument does not match that indicated by the string-value of the selected node; otherwise False.

/sakila/searchRentals?_s=returndate!=2005-08-31T20:00:25.000%2B06:00
SELECT * FROM rental WHERE returndate <> 'Wed Aug 31 08:00:25 MDT 2005'

“=lt=” yields True if the point in time specified in the argument follows that indicated by the string-value of the selected node; otherwise False.

/sakila/searchRentals?_s=rentaldate=lt=2005-05-27T00:00:00.000%2B00:00
SELECT * FROM rental WHERE rentaldate < 'Thu May 26 18:00:00 MDT 2005'

“=le=” yields True if the point in time specified in the argument follows that indicated by the string-value of the selected node, or is equal to it; otherwise False.

/sakila/searchRentals?_s=returndate=le=2005-05-27T00:00:00.000%2B00:00
SELECT * FROM rental WHERE returndate <= 'Thu May 26 18:00:00 MDT 2005'

“=gt=” yields True if the point in time specified in the argument precedes that indicated by the string-value of the selected node; otherwise False.

/sakila/searchRentals?_s=returndate=gt=2005-08-30T00:00:00.000%2B00:00
SELECT * FROM rental WHERE returndate > 'Mon Aug 29 18:00:00 MDT 2005'

“=ge=” yields True if the point in time specified in the argument precedes that indicated by the string-value of the selected node, or is equal to it; otherwise False.

/sakila/searchRentals?_s=returndate=ge=2005-09-01T00:00:00.000%2B00:00
SELECT * FROM rental WHERE returndate >= 'Wed Aug 31 18:00:00 MDT 2005'

Some advanced FIQL expressions prove to be very powerful when dealing with date range and relative comparison:

/sakila/searchRentals?_s=returndate=gt=%2DP5Y2M
SELECT * FROM rental WHERE returndate > 'Wed Aug 17 14:53:44 MDT 2005'
 (assuming processing on Oct 17th, 2010)

/sakila/searchRentals?_s=returndate=ge=2005-05-27T00:00:00.000%2B00:00;rentaldate=le=2005-06-27T00:00:00.000%2B00:00
SELECT * FROM rental WHERE rentaldate <= 'Sun Jun 26 18:00:00 MDT 2005' AND returndate >= 'Thu May 26 18:00:00 MDT 2005'

Four operators are relevant to numeric comparisons:

“==” yields True if the string-value of the selected node is numerically equal to the argument; otherwise False.

/sakila/searchFilms?_s=filmid==1;rentalduration!=0
SELECT * FROM film WHERE filmid = '1' AND rentalduration <> '0'

“!=” yields True if the string-value of the selected node is not numerically equal to the argument; otherwise False.

/sakila/searchFilms?_s=filmid!=1;rentalduration!=0
SELECT * FROM film WHERE filmid <> '1' AND rentalduration <> '0'

“=lt=” yields True if the string-value of the selected node evaluates as numerically less than the argument; otherwise, False.

/sakila/searchFilms?_s=filmid=lt=2;rentalduration!=0
SELECT * FROM film WHERE filmid < '2' AND rentalduration <> '0'

“=le=” yields True if the string-value of the selected node evaluates as numerically less than the argument, or as equal to it; otherwise, False.

/sakila/searchFilms?_s=filmid=le=2;rentalduration!=0
SELECT * FROM film WHERE filmid <= '2' AND rentalduration <> '0'

“=gt=” yields True if the string-value of the selected node evaluates as numerically greater than the argument; otherwise, False.

/sakila/searchFilms?_s=filmid=gt=995;rentalduration!=0
SELECT * FROM film WHERE filmid > '995' AND rentalduration <> '0'

“=ge=” yields True if the string-value of the selected node evaluates as numerically greater than the argument, or as equal to it; otherwise, False.

/sakila/searchFilms?_s=filmid=ge=995;rentalduration!=0
SELECT * FROM film WHERE filmid >= '995' AND rentalduration <> '0'

I believe this covers majority of the FIQL operators used in various proportions as documented in the spec and these FIQL expressions are not just examples, all of these searches can be tested with our sample Sakila database.

Sakila Database

Sakila is a MySQL sample database that represents a DVD rental store and comes with some useful data. This is handy for demonstrations and the schema is well designed. I will be using this sample database to demonstrate the advanced search capabilities using CXF. Java IDEs provide a decent support to reverse engineer JPA entities from a database schema. Here is one which walks through creating JPA entities using a NetBeans IDE from the Sakila database. I will be using these generated entities in this example with Hibernate as the JPA provider. The entire example is available as a maven project in Git, so feel free to check this out.

The meat of this example lies in the SakilaResource implemented as a CXF JAX-RS endpoint. In this code, a SearchContext is injected to the resource which will be used in the resource methods to get the SearchCondition specified in the query. FiqlParser does the magic for you in parsing an FIQL expression to construct these search conditions.

@Path("/sakila")
public class SakilaResource {

    @Context
    private SearchContext searchContext;

    List<Actor> actors = new ArrayList<Actor>();
    List<Film> films = new ArrayList<Film>();
    List<Rental> rentals = new ArrayList<Rental>();

    public SakilaResource() {
       // JPA Plumbing omitted for clarity.
    }

    @GET
    @Produces("application/xml")
    @Path("searchActors")
    public List<Actor> searchActors() {
        SearchCondition<Actor> sc = searchContext.getCondition(Actor.class);

        if (sc == null) {
            throw new NotFoundException("Invalid search query.");
        }
        System.out.println(sc.toSQL("actor"));

        List<Actor> found = sc.findAll(actors);
        if (found.size() == 0) {
            throw new NotFoundException("No matching actor found.");
        }
        return found;
    }

    @GET
    @Produces("application/xml")
    @Path("searchFilms")
    public List<Film> searchFilms() {
        SearchCondition<Film> sc = searchContext.getCondition(Film.class);

        if (sc == null) {
            throw new NotFoundException("Invalid search query.");
        }
        System.out.println(sc.toSQL("film"));

        List<Film> found = sc.findAll(films);
        if (found.size() == 0) {
            throw new NotFoundException("No matching film found.");
        }
        return found;
    }

    @GET
    @Produces("application/xml")
    @Path("searchRentals")
    public List<Rental> searchRentals() {
        SearchCondition<Rental> sc = searchContext.getCondition(Rental.class);

        if (sc == null) {
            throw new NotFoundException("Invalid search query.");
        }
        System.out.println(sc.toSQL("rental"));

        List<Rental> found = sc.findAll(rentals);
        if (found.size() == 0) {
            throw new NotFoundException("No matching rental found.");
        }
        return found;
    }
}

One can think of FIQL based search support in CXF is somewhat similar to using Hibernate Search to query JPA entities. I think Hibernate Search is a heavy weight solution which uses Lucene as the search provider and it requires annotating your entities. FIQL support in CXF is light weight and implementing advanced search capabilities in resources is a child’s play. Unlike Hibernate Search, CXF FIQL cannot be used to perform joins on multiple entities as the FIQL spec was designed for syndication feed data model.

In CXF, there are couple options to test these search conditions and they are easy to develop. One approach is to use the search extension API. You can find an example in the javadoc of SimpleSearchCondition.

The other approach is the traditional CXF way of using HTTP centric WebClient.

public class SakilaSearchTest {

    static Server server;

    @BeforeClass
    public static void setUp() {
        JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
        sf.setResourceClasses(SakilaResource.class);
        sf.getInInterceptors().add(new LoggingInInterceptor());
        sf.getOutInterceptors().add(new LoggingOutInterceptor());
        sf.setResourceProvider(SakilaResource.class, new SingletonResourceProvider(new SakilaResource()));
        sf.setAddress("http://localhost:9000");
        server = sf.create();
    }

    @Test
    public void searchActors() {
        //http://localhost:9000/sakila/searchActors?_s=firstname==PENELOPE
        WebClient wc = WebClient.create("http://localhost:9000/sakila/searchActors?_s=firstname%3D%3DPENELOPE");
        Collection<? extends Actor> actors = wc.getCollection(Actor.class);
        assertEquals(4, actors.size());
    }

    @Test
    public void searchFilms() {
        //http://localhost:9000/sakila/searchFilms?_s=rating==PG;rentalduration!=0;title==SANTA*
        WebClient wc = WebClient.create("http://localhost:9000/sakila/searchFilms?_s=rating%3D%3DPG;rentalduration%21%3D0;title%3D%3DSANTA*");
        Collection<? extends Film> films = wc.getCollection(Film.class);
        assertEquals(1, films.size());
    }

    @Test
    public void searchRentals() {
        //http://localhost:9000/sakila/searchRentals?_s=rentaldate=lt=2005-05-27T00:00:00.000%2B00:00
        WebClient wc = WebClient.create("http://localhost:9000/sakila/searchRentals?_s=rentaldate%3Dlt%3D2005-05-27T00:00:00.000%2B00:00");
        Collection<? extends Rental> rentals = wc.getCollection(Rental.class);
        assertEquals(278, rentals.size());
    }

    @AfterClass
    public static void tearDown() {
        server.destroy();
    }
}

One thing I noticed with WebClient is that I need to encode certain operators specified in the search condition and this is inconsistent when testing with a web browser. I would expect the java API should take care of this for you instead of providing partially encoded URLs. Also, the date comparison can be tricky, although it works for most cases discussed above.

Possibly Related Posts: