Sunday, April 5, 2015

Using Camel Rest DSL to Create REST API

It used to be pretty fiddly to create a rest interface in camel. As of Camel 2.14 a new Rest DSL is available for creating Rest API Endpoints in Camel. I wanted to create rest endpoints to manage sending both config and control data to my PiHex robot.

My design uses an eventbus to handle different events from different sources. For example; updating servo calibration data via rest, loading config from persistent storage receiving controller input via rest or via radio control. There will be more.

At this point I want to focus on adding the rest support. In particular for getting and updating the servo config model object. I've currently kept this very simple. Each servo has it's one config url http://mypi:port/servoconfig/{channel} where 'servoconfig' is the entity and id is the numeric id of the servo channel.

@XmlRootElement
public class ServoConfig {

    private static final Logger log = LoggerFactory.getLogger(ServoConfig.class);

    public static final int DEFAULT_RANGE = 180;
    public static final int MIN_PULSE = 1000;
    public static final int MAX_PULSE = 2000;

    private int channel;
    // range 180 -90 = 1000 90 = 2000 0 = 1500 0-90 = 500
    private int range;
    private int center;
    private int lowLimit = Integer.MIN_VALUE; // can't initialize to 0, 0 is a valid limit
    private int highLimit = Integer.MAX_VALUE; // can't initialize to 0, 0 is a valid limit
    private String name;

    public String getName() {...}

    public void setName(String name) {...}

    public int getChannel() {...}

    public void setChannel(int channel) {...}

    public int getRange() {...}

    public void setRange(int range) {...}

    public int getCenter() {...}

    public void setCenter(int center) {...}

    public int getLowLimit() {...}

    public void setLowLimit(int lowLimit) {...}

    public int getHighLimit() {...}

    public void setHighLimit(int highLimit) {...}

The model class ServoConfig is a basic pojo, nothing fancy just annotated with '@XmlRootElement'. I want to be able to view the current config of a servo (GET) and update the config (PUT). These are the only two methods I need. To create the route I am using Jetty for the Http Server and Jackson for the JSON support. To add these we need to add the following to the pom.xml.

        
            org.apache.camel
            camel-jetty
            ${camel.version}
        
        
            org.apache.camel
            camel-jackson
            ${camel.version}
        
Of course the version has to be 2.14 or later. I am using 2.15.0.

Now we can create a rest endpoint in camel. My endpoints are super simple for the robot I have the endpoints for the ServoConfig in a single camel route.

public class ServoConfigRouteBuilder extends RouteBuilder {

    @Override
    public void configure() throws Exception {
        getContext().getTypeConverterRegistry().addTypeConverter(ServoConfig.class, Servo.class, new ServoConfigTypeConverter());
        restConfiguration().component("jetty").host("{{config:com.margic.pihex.api.address}}").port("{{config:com.margic.pihex.api.port}}").bindingMode(RestBindingMode.auto);

        rest("/servoconfig/")
                .get("/{channel}")
                .outType(ServoConfig.class)
                .to("direct:getServoConfig")
                .put("/{channel}")
                .consumes("application/json")
                .type(ServoConfig.class)
                .to("direct:putServoConfig");

        from("direct:getServoConfig")
                .routeId("getServoConfig")
                .setBody(header("channel"))
                .to("bean:controller")
                .convertBodyTo(ServoConfig.class);

        // writes servo calibration to file one per servo for now. will consolidate later
        from("direct:putServoConfig")
                .routeId("putServoConfig")
                .multicast()
                    .to("direct:updateRunningConfig")
                    .to("direct:writeConfigToFile")
                .end()
                .setHeader(Exchange.HTTP_RESPONSE_CODE, constant("204"));

Camel is doing a lot of magic for us here. Rather than the usual DSL endpoint starting with "from" we use "rest" when using the Rest DSL. My GET and PUT http verb rest operations are built using the fluent Rest DLS using .get and .put respectively.

Stepping through the dsl:
rest("/servoconfig/")  // sets the entity path to servoconfig
    .get("/{channel}") // assigns the HTTP get with the path to the variable channel id
    .outType(ServoConfig.class) // sets the return type for the get request
    .to("direct:getServoConfig") // sets the endpoint to send this request to
    .put("/{channel}") // assigns the HTTP put with the path to the variable channel id
    .consumes("application/json") // specify the required media type that matches this route
    .type(ServoConfig.class) // type of the incoming body that the mapper should use to unmarshall the json
    .to("direct:putServoConfig"); // where to send this put request

The Rest DSL does not do the work itself it relies on rest component to implement the endpoints. This is specified in the route builder using the restConfiguration method.

restConfiguration().component("jetty").host("{{config:com.margic.pihex.api.address}}").port("{{config:com.margic.pihex.api.port}}").bindingMode(RestBindingMode.auto);


In this case I am setting the component to Jetty as mentioned and using property placeholders to set the properties for address and port. I like to use placeholders for things like the port it makes it much easier to manage and makes testing easier.

The .bindingMode method sets the way camel will manage binding to and from POJO objects. This is really important. The default is not to bind. You have to specify something here if you want Camel to do the work for you. I use auto mode and specify media types and object types. Don't forget to set something in your route or you'll have to handle this in your route. My objects are so simple I want camel to do most of the heavy lifting for me.

Testing the Camel Rest DSL endpoint.

Testing the endpoint can be done by overriding the endpoints. I personally like to test rest endpoints directly by using an http client as I frequently get issues with media conversions etc and testing with the client is relatively simple. Apache http components have introduced a new fluent http client api that makes this even easier. 

This is my test case to test the GET message.

public class ServoConfigRouteGetBuilderTest extends CustomCamelContextTestSupport {
    private int port;

    @Override
    protected RouteBuilder createRouteBuilder() throws Exception {
        log.info(System.getProperties().toString());
        port = AvailablePortFinder.getNextAvailable(8080);
        setConfigurationProperty("com.margic.pihex.api.port", Integer.toString(port));
        setConfigurationProperty("com.margic.pihex.servo.conf", "${sys:user.dir}/target/test-classes/confwritetest/conf/");
        return new ServoConfigRouteBuilder();
    }
    @Test
    public void testServoConfigGet() throws Exception {
        String content = Request.Get("http://localhost:" + port + "/servoconfig/0")
                .addHeader("Accept", "application/json")
                .execute()
                .returnContent()
                .asString();
        log.info(content);
        JSONAssert.assertEquals("{\"name\":\"Leg 0 Coxa\",\"channel\":0,\"range\":180,\"center\":0,\"lowLimit\":-90,\"highLimit\":90}", content, true);
    }
}

My GET use case is pretty simple I don't need to mock the object being return as there is always a default config for a servo. What is interesting here is the http Request.Get fluent method. The new fluent builder makes it very easy to test the call without to having to worry about creating the client or releasing the resources.

Mapping path params to Camel Messages

In my example above I have the path param {channel} for both my get and put requests. Camel Rest DSL maps the path param to a camel header of the same name as can be seen in the debug below of the incoming message my get route.


No comments:

Post a Comment