CDI - Payload transformation
tfennelly Nov 29, 2010 7:42 AMIn fact, this is a wider discussion than CDI, but I've been looking at it as part of the CDI integration work, so we can discuss it in that context for now.
First off… ultimately, I think we'll need to support the definition of transformations in a number of ways, but I'm only exploring one option here. The option I'm looking at here is a code based option, where a service developer can define @Transformer classes that have simple @From and @To annotations on transformation methods. These transformation methods are registered in a very simple TransformRegistry class, which is injected into each ServiceProxyHandler instance (the ServiceProxyHandler is the ExchangeHandler impl that accepts an ESB Exchange instance on behalf of a Service bean, unwraps the Exchange and invokes the appropriate Service bean method).
When the handle method of the the ServiceProxyHandler is invoked, it checks the exchange context for the name of the target operation on the Service bean, as well as message type information (currently calling this a "PayloadSpec"). Yhe PayloadSpecs are very simple opaque string values that specify the IN and OUT data types of the Exchange. IN is the data type the invoker is sending in on the Exchange, while OUT is the data type the invoker wants/expects back.
For invoking the service, the ServiceProxyHandler gets the IN PayloadSpec (the @From) and the target operation's param sig (the @To), looks up and invokes the appropriate transformer method in the TransformRegistry. This transforms the payload @From the Exchange type, @To the Service methods param type (currently only supporting a single arg Service bean method). After invoking the Service bean method (for IN_OUT exchanges), the ServiceProxyHandler applies the same basic principles again, using the Service bean's return value type (the @From) and the Exchange's OUT PayloadSpec (the @To) to lookup and perform a transform on the response.
So as an example, we can look at the unit tests where we have the following service...
@Service public class WithProductsOrderManagementService { @Inject @Service private ProductService productService; public OrderResponse createOrder(OrderRequest request) { OrderResponse orderResponse = new OrderResponse(request.orderId); orderResponse.product = productService.getProduct(request.productId); return orderResponse; } }
It has a single "createOrder" operation that takes a OrderRequest as input and returns an OrderResponse instance as output. One of the tests uses a simple SOAP message to invoke this service and expects a SOAP response back. The SOAP request and expected response look as follows….
Request: <soap11:Envelope xmlns="urn:createOrderRequest:v1" xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/"> <soap11:Body> <createOrder> <orderId>D123</orderId> <productId>ABCD</productId> </createOrder> </soap11:Body> </soap11:Envelope>
Response: <soap11:Envelope xmlns="urn:createOrderResponse:v1" xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/"> <soap11:Body> <createOrderResponse> <orderId>D123</orderId> <productId>ABCD</productId> <productName>MacBook Pro</productName> </createOrderResponse> </soap11:Body> </soap11:Envelope>
For these, we define the PayloadSpecs to be "urn:createOrderRequest:v1:soap" and "urn:createOrderResponse:v1:soap" respectively. For this, we need 2 transform methods. The first to convert @From "urn:createOrderRequest:v1:soap" @To an OrderRequest instance and the second to convert @From an OrderResponse instance @To "urn:createOrderResponse:v1:soap". For this we defined the following Transformer class and added 2 transform methods as follows:
@Transformer public class OrderModelTransforms { private Smooks readSOAP_V1; private Smooks writeSOAP_V1; public OrderModelTransforms() throws IOException, SAXException { readSOAP_V1 = new Smooks(getClass().getResourceAsStream("createOrderRequest_v1_read.xml")); writeSOAP_V1 = new Smooks(getClass().getResourceAsStream("createOrderResponse_v1_write.xml")); } public OrderRequest readSOAP_V1(@From("urn:createOrderRequest:v1:soap") String inXML) { JavaResult javaResult = new JavaResult(); readSOAP_V1.filterSource(new StringSource(inXML), javaResult); return javaResult.getBean(OrderRequest.class); } public void writeSOAP_V1(@From OrderResponse orderResponse, @To("urn:createOrderResponse:v1:soap") Writer outXML) { JavaSource source = new JavaSource(orderResponse); source.setEventStreamRequired(false); writeSOAP_V1.filterSource(source, new StreamResult(outXML)); } }
In this case we used Smooks to perform both transformations, but you can obvsiously use whatever you like (JAXB etc).
In the first method (readSOAP_V1) you'll note we don't specify a @To annotation. This is implicit in the OrderRequest return type of the method.
Invoking the Service in this case is a matter of setting SOAP as the exchange message payload and then setting 2 Exchange Context properties specifying the target operation name + the IN and OUT PayloadSpecs:
Exchange exchange = domain.createExchange(new QName("WithProductsOrderManagementService"), ExchangePattern.IN_OUT, responseConsumer); ServiceProxyHandler.setOperationName(exchange, "createOrder"); PayloadSpec.setInPayloadSpec(exchange, "urn:createOrderRequest:v1:soap"); PayloadSpec.setOutPayloadSpec(exchange, "urn:createOrderResponse:v1:soap");
I didn't want to add annotations on the service itself (to point to transformation logic) as that seemed to be too tight a coupling, but also, it seems logical to me that we will need to be able to perform different transformations, depending on the source of the service invocation.
Another option I'd be interested in exploring is how we might hook this into the ExchangeImpl itself, so that when we call the Exchange.getContent(Class<T> type) we can apply an appropriate transformation based on the type/PayloadSpec of the content of the Exchange Message (the @From) and the Class type specified in the getContent call (the @To).