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.
Of course the version has to be 2.14 or later. I am using 2.15.0.org.apache.camel camel-jetty ${camel.version} org.apache.camel camel-jackson ${camel.version}
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