Apache CXF is a part of JBoss Fuse, so is Apache Camel. In this blog post we are going to implement a rest client in CXFRS and Camel. Note, you can also implement a rest client using JAX-RS directly, but in this blog post we are using the CXFRS framework.
For this example we are going to implement a client for the public rest api: http://freegeoip.net
From the freegeoip website:
“The HTTP API takes GET requests in the following schema:
freegeoip.net/{format}/{IP_or_hostname}
Supported formats are: csv, xml, json and jsonp. If no IP or hostname is provided, then your own IP is looked up. ”
To create the CXFRS client endpoint and the Camel route using it we will have to complete the following steps.
- Add Maven dependencies to the Pom file
- Implement the CXFRS client proxy
- setup a CxfRsClient Endpoint in Camel
- Create a custom processor for formatting the CXFRS request
- Create a Camel route to call the CXFRS endpoint
Add Maven dependencies
Assuming you start out with a Camel or Fuse project the following dependencies should be added to the POM file:
<dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm-all</artifactId> <version>4.1</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http-jetty</artifactId> <version>${camel-version}</version> </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-cxf</artifactId> <version>${camel-version}</version> </dependency>
Implement the CXFRS client proxy
The Camel CXFRS components can either use the CXFRS client proxy API or the HTTP API. When using the entire http request needs to be created in Camel. This option is basically the same as the http component in Camel. Therefore in this post we are going to implement the CXFRS client proxy API.
More information on the CXFRS client proxy API can be found on the CXF website: https://cwiki.apache.org/confluence/display/CXF20DOC/JAX-RS+Client+API#JAX-RSClientAPI-Proxy-basedAPI
To implement the client proxy create a new Java interface (a class will work as well) but since the client proxy is only using empty methods an interface will be fine.
The name of the class/interface can be whatever you want, but since we are making a client for freegeoip I called the interface freegeoip.
@Path(value="/") public interface Freegeoip { …
Next we need to annotate the interface with the Path annotation, the value of the path annotation will correspond to the path after the root url. The value “/” corresponds with the root, so with: http://freegeoip.net
Next we need to add a method for specific request we want to implement. Note, you can add as many methods as you want, and if the rest API you want to call actually supports them of cource
In our example we need to implement the API call of freegeoip: freegeoip.net/{format}/{IP_or_hostname}
To do this add a method to our interface, again the name of the method can be whatever you like, here I chose “getGeoip”:
@GET @Path(value="/{type}/{ip}") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) public String getGeoip(@PathParam("type") String type, @PathParam("ip") String ip);
The method is annotaded with the rest http operation it uses, in our example GET. Again with a Path corresponding to the specific path of that particular request. Note that we are using variables in the path value: type and ip. The last annotation we use is the response we expect back from the rest API. In this example both XML and JSON.
Next is the method signature, note the parameters of the method are also annotated with the PathParam annotation. The values of the PathParam annotations corresponds with the values of the variables in the Path annotation. This means the arguments of the methods will be bound to the variables of the path when making the request.
The entire java interface looks like this:
package nl.rubix.cxfrs.test.endpoint; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path(value="/") public interface Freegeoip { @GET @Path(value="/{type}/{ip}") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) public String getGeoip(@PathParam("type") String type, @PathParam("ip") String ip); }
setup a CxfRsClient Endpoint in Camel
To setup the endpoint create a new Camel context xml file. You can use either Spring of Blueprint, although the configuration differs slightly. In this example I am using Blueprint.
The first thing we need to do is to add the CXF namespace to the Blueprint xml:
xmlns:cxf=”http://camel.apache.org/schema/blueprint/cxf”
This enables the CXF options in the Blueprint xml. So now a CxfRsClient can be created. Add this client to the Blueprint XML (outside the CamelContext):
<cxf:rsClient id="rsClient" address="http://freegeoip.net" serviceClass="nl.rubix.cxfrs.test.endpoint.Freegeoip" loggingFeatureEnabled="true"/>
The request to the CXF Client API has some specific requirements. To implement these requirements we use a custom processor. To create a custom processor create a new Java class implementing the Processor interface. Inside the processor we do three things: set the exchange pattern to inOut; setting some CXF headers; create the request object expected by CXF.
The argument of the process method is the exchange object (this method is required when implementing the Processor interface). So manipulating the exchange pattern and headers is quite straightforward. To set the exchange pattern to inOut:
exchange.setPattern(ExchangePattern.InOut);
Next we need tot set the message header to state the operation name we want to call. This operation name corresponds with the method name of our Client Proxy API. We also want to use the Client proxy API in stead of the HTTP API (see above).
To set these headers add the following statements to the processor:
// set the operation name inMessage.setHeader(CxfConstants.OPERATION_NAME, "getGeoip"); // using the proxy client API inMessage.setHeader(CxfConstants.CAMEL_CXF_RS_USING_HTTP_API, Boolean.FALSE);
Now all the headers and exchange properties are set properly we move our attention to the message body. By default CXFRS expects the request to be of the type, org.apache.cxf.message.MessageContentsList and although this can be change by creating a custom binding in this post we are going to stick with the MessageContentsList type.
The MessageContentsList is a name value pair type and should contain (in order) the arguments of the method in our Client Proxy API. The method signature of our Client Proxy API looks like this:
public String getGeoip(@PathParam("type") String type, @PathParam("ip") String ip);
So we need to add two Strings to our MessageContentsList: type and ip.
To do this add the following code to the process method:
String ip = inMessage.getBody(String.class); String type = inMessage.getHeader("type",String.class); MessageContentsList req = new MessageContentsList(); req.add(type); req.add(ip); inMessage.setBody(req);
In our example we get the ip address from the message body and the type out of a header. These values we will set when we implement our Camel route. It is likely that in a normal situation more than one field will be present in the body as some sort of type and a more sophisticated data mapping has to be implemented. However, in this example we are keeping it simple with a one-to-one mapping of the request body to one of the entries in the MessageContentsList.
The entire processor class looks like this:
package nl.rubix.cxfrs.test.transform; import org.apache.camel.Exchange; import org.apache.camel.ExchangePattern; import org.apache.camel.Message; import org.apache.camel.Processor; import org.apache.camel.component.cxf.common.message.CxfConstants; import org.apache.cxf.message.MessageContentsList; public class RequestProcessor implements Processor{ @Override public void process(Exchange exchange) throws Exception { exchange.setPattern(ExchangePattern.InOut); Message inMessage = exchange.getIn(); // set the operation name inMessage.setHeader(CxfConstants.OPERATION_NAME, "getGeoip"); // using the proxy client API inMessage.setHeader(CxfConstants.CAMEL_CXF_RS_USING_HTTP_API, Boolean.FALSE); //creating the request String ip = inMessage.getBody(String.class); String type = inMessage.getHeader("type",String.class); MessageContentsList req = new MessageContentsList(); req.add(type); req.add(ip); inMessage.setBody(req); } }
Now that our processor is finished we can implement the Camel route calling this processor and the actual REST endpoint.
Create a Camel route to call the CXFRS endpoint
We already created the Camel context and added the CXFRS endpoint to it in the step: “setup a CxfRsClient Endpoint in Camel” now we can implement the Camel route using the CxfRsClient endpoint and the processor we created earlier.
To start off the route we will just use the timer component so our route starts off automatically.
<from uri="timer://foo?period=5000"/>
Next up we need to prepare the request, so we need to set the header “type” containing either xml or json. And we need to set the body to the ip address we want to lookup in the REST API (note: I changed my personal ip address to 0.0.0.0.
<setHeader headerName="type"> <constant>xml</constant> </setHeader> <setBody> <constant>0.0.0.0</constant> </setBody>
Now that the request body and header are set in the Camel route we can call the processor we created in the previous step. (normally these values would not be hardcoded off course )
To call our processor we first need to declare this processor as a bean in our Blueprint xml. So outside the CamelContext xml tags add the bean declaration:
<bean id="requestProcessor" class="nl.rubix.cxfrs.test.transform.RequestProcessor"/>
After our bean is declared inside the Camel route call the processor:
<process ref="requestProcessor"/>
Now everything is setup properly to make the call to our CxFRsClientEndpoint:
<to uri="cxfrs:bean:rsClient"/>
The entire Blueprint xml file looks like this:
<?xml version="1.0" encoding="UTF-8"?> <blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0" xmlns:camel="http://camel.apache.org/schema/blueprint" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cxf="http://camel.apache.org/schema/blueprint/cxf" xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd http://camel.apache.org/schema/blueprint http://camel.apache.org/schema/blueprint/camel-blueprint.xsd"> <bean id="requestProcessor" class="nl.rubix.cxfrs.test.transform.RequestProcessor"/> <camelContext trace="false" xmlns="http://camel.apache.org/schema/blueprint"> <route> <from uri="timer://foo?period=5000"/> <log message="Starting Route with client proxy api"/> <setHeader headerName="type"> <constant>xml</constant> </setHeader> <setBody> <constant>0.0.0.0</constant> </setBody> <process ref="requestProcessor"/> <to uri="cxfrs:bean:rsClient"/> <log message="${body}"/> </route> </camelContext> <cxf:rsClient id="rsClient" address="http://freegeoip.net" serviceClass="nl.rubix.cxfrs.test.endpoint.Freegeoip" loggingFeatureEnabled="true"/> </blueprint>
Now when we run the route we see our log message (again I changed my personal ip and the coordinates in the response message to 0s):
[mel-2) thread #1 - timer://foo] LoggingOutInterceptor INFO Outbound Message --------------------------- ID: 1 Address: http://freegeoip.net/xml/0.0.0.0 Http-Method: GET Content-Type: application/xml Headers: {Content-Type=[application/xml], Accept=[application/json, application/xml]} -------------------------------------- [mel-2) thread #1 - timer://foo] LoggingInInterceptor INFO Inbound Message ---------------------------- ID: 1 Response-Code: 200 Encoding: ISO-8859-1 Content-Type: application/xml Headers: {Access-Control-Allow-Method=[GET, HEAD, OPTIONS], Access-Control-Allow-Origin=[*], connection=[keep-alive], Content-Length=[367], content-type=[application/xml], Date=[Mon, 01 Dec 2014 11:11:05 GMT], Server=[nginx/1.4.6 (Ubuntu)], X-Database-Date=[Wed, 26 Nov 2014 13:21:26 GMT]} Payload: <?xml version="1.0" encoding="UTF-8"?> <Response> <IP>0.0.0.0</IP> <CountryCode>NL</CountryCode> <CountryName>Netherlands</CountryName> <RegionCode></RegionCode> <RegionName></RegionName> <City></City> <ZipCode></ZipCode> <TimeZone>Europe/Amsterdam</TimeZone> <Latitude>0.0</Latitude> <Longitude>0.0</Longitude> <MetroCode>0</MetroCode> </Response>
And when we change the header type to json:
D: 1 Address: http://freegeoip.net/json/0.0.0.0 Http-Method: GET Content-Type: application/xml Headers: {Content-Type=[application/xml], Accept=[application/json, application/xml]} -------------------------------------- [mel-2) thread #1 - timer://foo] LoggingInInterceptor INFO Inbound Message ---------------------------- ID: 1 Response-Code: 200 Encoding: ISO-8859-1 Content-Type: application/json Headers: {Access-Control-Allow-Method=[GET, HEAD, OPTIONS], Access-Control-Allow-Origin=[*], connection=[keep-alive], Content-Length=[208], content-type=[application/json], Date=[Mon, 01 Dec 2014 11:16:03 GMT], Server=[nginx/1.4.6 (Ubuntu)], X-Database-Date=[Wed, 26 Nov 2014 13:21:26 GMT]} Payload: {"ip":"0.0.0.0","country_code":"NL","country_name":"Netherlands","region_code":"","region_name":"","city":"","zip_code":"","time_zone":"Europe/Amsterdam","latitude":0.0,"longitude":0.0,"metro_code":0}