Modeling and Implementing a Tuscany Application out of a WSDL File
Contributed by Antollini, Mario <mario.antollini@intel.com>
The main focus of this document is to report and share the experience gained after creating a new Tuscany application whose starting point was a WSDL file. In other words, this document will serve as guide for those interested in creating a Tuscany application out of a given WSDL. It not only describes the steps followed but also mentions the difficulties faced and their resolution. Moreover, it could also serve as the ground basis for creating a WSDL2Composite tool (à la Axis2's WSDL2Java).
Introduction
In the same way the business logic for a Web Service can be created following a top-down approach (i.e. specify the interface first, then write the code which sticks to it), we had the need to create a Tuscany application which adhered to a Web Service Interface already defined.
What we tried to accomplished was the challenge to model and implement an Amazon-like application running as a Tuscany Application (from now on Shopping Store). Of course, we wanted this application to be fully compatible to the Amazon's WSDL (http://ecs.amazonaws.com/AWSECommerceService/2007-05-14/AWSECommerceService.wsdl). This way, any given (Web) application written to interact with Amazon through its WS interface (e.g. http://www.openlaszlo.org/node/198
) would be seamlessly communicating with the application entirely coded as a Tuscany application.
Having understood the intention of this document we can proceed to detail the steps carried out to build the application.
Specifying the Web Service Interface
Following a top-down approach, the first step is to define the interface of the Tuscany service/s which will be exposed as Web Services. Once the interfaces are defined, a WSDL file which expresses/describes the methods, its parameters and returned types must be written.
In the particular case of the application which gave birth to this document, the WSDL file was taken from Amazon. It is important to make clear that in its first phase of development the Shopping Store application was intended to provide only a Shopping Cart service, and this document only covers this initial phase. Therefore, since Amazon's WSDL is composed of a lot of different methods/operations it had to be reduced and only the operations related to the Shopping Cart remained. Here you can see the final version of the WSDL file called shoppingstore.wsdl: https://issues.apache.org/jira/secure/attachment/12368949/ShoppingStore.wsdl. It is worth mentioning that the soap address was modified in order for it to point to the location where the Shopping Cart service will be running.
Generate Stub and Skeleton Code
Now, what we need to do is to start writing the source code which offers the functionality/operations offered by the WSDL.
The first natural thing to do is to translate the interfaces of the Shopping Store application (which only exposes the Shopping Cart service so far) to the Java language. In order to accomplish that we will use the wsdl2java tool available in Axis2 (version 1.2). We will use this tool for two purposes:
1 - Generate the stub code ("client-side") which will facilitate the effort required to develop a standalone Java client which will interact with the Shopping Cart via WS invocations.
2 - Generate the skeleton code ("server-side") which we will only use to understand the (Java) interface our Shopping Cart service will stick to.
What we need to do is to create an script file with the following content (assuming Axis2-1.2 was already downloaded, unzipped and the %AXIS2_HOME% environment variable has been set):
Windows:
set CURRENT_DIR=. setlocal EnableDelayedExpansion set AXIS2_CLASS_PATH=%AXIS2_HOME% FOR %%c in ("%AXIS2_HOME%\lib\*.jar") DO set AXIS2_CLASS_PATH=!AXIS2_CLASS_PATH!;%%c java -cp "%AXIS2_CLASS_PATH%" org.apache.axis2.wsdl.WSDL2Java -d jaxbri -ss -ssi -g -uri .\ShoppingStore.wsdl endlocal
Linux:
AXIS2_CLASSPATH="" for f in "$AXIS2_HOME/lib/*.jar" do AXIS2_CLASSPATH="$AXIS2_CLASSPATH":$f done java -cp $AXIS2_CLASSPATH org.apache.axis2.wsdl.WSDL2Java -d jaxbri -ss -ssi -g -uri ./ShoppingStore.wsdl
Note that the data binding used was JAXB (denoted by the option "-d jaxbri"). Different errors/problems occurred at different stages of the development of the ToyApp when other data bindings were used. JAXB was the one which presented the less inconveniences throughout the development of the ToyApp.
Place the script file in the same directory where the WSDL file is located and then run it. Once the script finishes executing, the following directory structure will be created in the current directory:
<CURRENT_DIR>/src/com/amazon/webserices/awsecommerceservice/_2007_05_14
and many files will show up in there.
After running the script commands, it is necessary to modify the source code that has been generated, otherwise we will get runtime errors (very difficult to solve at that time actually). What we need to do is to look for the line * @XmlType(name = "", propOrder = { * in the following files and add their names within the empty quotes: CartAdd.java, CartAddResponse.java, CartClear.java, CartClearResponse.java, CartCreate.java, CartCreateResponse.java, CartGet.java and CartGetResponse.java. For example, in the case of CartAdd.java, the string * @XmlType(name = "", propOrder = { * must be replaced by * @XmlType(name = "CartAdd", propOrder = { *, and in the case of CartCreateResponse.java, it must look like * @XmlType(name = "CartCreateResponse", propOrder = { *.
All the generated files must be used for marshaling and unmarshaling purposes, therefore they must be included at both compile- and run-time. Thus, the best thing to do is to compile the generated code and pack it into a jar file. In order for it to compile, we need to include the following jars in a "lib" directory: axiom-api-1.2.4.jar, axis2-kernel-1.2.jar, jaxb-api-2.0.2.jar, stax-api-1.0.1.jar and wsdl4j-1.6.2.jar. Also create an empty directory called "build" and another one called "dist" and then add the following lines to a build script:
Windows:
javac .\src\com\amazon\webservices\awsecommerceservice\_2007_05_14\*.java -cp .\lib\jaxb-api-2.0.2.jar;.\lib\stax-api-1.0.1.jar;.\lib\axiom-api-1.2.4.jar;.\lib\axis2-kernel-1.2.jar;.\lib\wsdl4j-1.6.2.jar;.\build -d build jar cvfM .\dist\AWS2007_05_14.jar -C .\build .
Linux:
javac ./src/com/amazon/webservices/awsecommerceservice/_2007_05_14/*.java -cp `cygpath -wp ./lib/jaxb-api-2.0.2.jar:./lib/stax-api-1.0.1.jar:./lib/axiom-api-1.2.4.jar:./lib/axis2-kernel-1.2.jar:./lib/wsdl4j-1.6.2.jar:./build` -d build -Xlint:unchecked jar cvfM ./dist/AWS2007_05_14.jar -C ./build .
After executing it, a file called AWS2007_05_14.jar will be located in the "dist" directory. It can be used in both both client and server side projects to reduce coding effort.
Coding Server-Side Business Logic
Even though we now count with a library which is in charge of marshaling and unmarshalling the remote invocations, we still need to take a look at one of its source files: ShoppingStoreServiceSkeletonInterface.java. This file is very useful for the server-side code since it describes the method names, their parameters and the return type of the operations the service will expose. Here you can see (a neat version of) the generated file:
/** * ShoppingStoreServiceSkeletonInterface.java */ package com.amazon.webservices.awsecommerceservice._2007_05_14; public interface ShoppingStoreServiceSkeletonInterface { public CartGetResponse CartGet (CartGet cartGet); public CartAddResponse CartAdd(CartAdd cartAdd); public CartCreateResponse CartCreate(CartCreate cartCreate); public CartModifyResponse CartModify(CartModify cartModify); public CartClearResponse CartClear(CartClear cartClear); }
Take into account that this skeleton file was generated using the reduced version of the Amazon WSDL, therefore it only contains the methods related to the Shopping Cart service.
We are now ready to implement the business logic for the Shopping Cart service...
First, let us create the following directory structure (this will be a new "project", thus create it in a new location):
- build -> this directory will contain everything needed to generate the jar containing the Shopping Store app.
- wsdl
- shoppingstore.wsdl -> the reduced Amazon WSDL
- shoppingstore.composite -> the content of this file will be explained later
- wsdl
- dist -> this directory will contain everything needed to run the Shopping Store application
- lib -> lib required to compile and run the Shopping Store application
- AWS2007_05_14.jar -> the jar file we have just created
- src -> directory containing the source code of the Shopping Store application (we will be explain it later)
- shoppingstore
- server
- ShoppingStoreServer.java
- services
- cart
- CartService.java
- CartServiceImpl.java
- cart
- server
- shoppingstore
Now, let us specify its interface (which, in this case, will be identical to the skeleton shown before) in a java file named CartService.java:
package shoppingstore.services.cart; import org.osoa.sca.annotations.Remotable; import com.amazon.webservices.awsecommerceservice._2007_05_14.*; @Remotable public interface CartService { public CartCreateResponse CartCreate(CartCreate cartCreate); public CartAddResponse CartAdd(CartAdd cartAdd); public CartModifyResponse CartModify(CartModify cartModify); public CartClearResponse CartClear(CartClear cartClear); public CartGetResponse CartGet(CartGet cartGet); }
The code was annotated with the @Remotable annotation. It indicates that the interface is designed to be used for remote communication. Remotable interfaces are intended to be used for coarse grained services. Operations parameters and return values are passed by-value.
Now, we need to implement the above methods. A proper implementation of the shopping cart logic is out of scope of this document; therefore, let us just implement a basic cart service which only expects one item per invocation to the CartAdd method and which just stores the cart and its items in main memory. Let us create a new file called CartServiceImpl.java:
package shoppingstore.services.cart; import org.osoa.sca.annotations.Scope; import java.util.HashMap; import com.amazon.webservices.awsecommerceservice._2007_05_14.*; @Scope("COMPOSITE") public class CartServiceImpl implements CartService { private static long ID = 0; //Associate usedID with CartInstance private HashMap<String, Cart> cartsHash = new HashMap<String, Cart>(); public CartCreateResponse CartCreate(CartCreate cartCreate) { CartCreateResponse cartCreateResponse = new CartCreateResponse(); Cart cart = getCart(cartCreate.getAWSAccessKeyId()); //If user has already created a cart during this session if(cart != null){ cartCreateResponse.getCart().add(cart); return cartCreateResponse; } cart = new Cart(); cart.setCartId(this.generateID()); cart.setCartItems(new CartItems()); //Add Cart to the HashMap addCart(cartCreate.getAWSAccessKeyId(), cart); cartCreateResponse.getCart().add(cart); return cartCreateResponse; } public CartAddResponse CartAdd(CartAdd cartAdd) { CartAddResponse cartAddResponse = new CartAddResponse(); CartAddRequest cartAddRequest = cartAdd.getRequest().get(0); //Retreive the cart associated with this user Cart cart = getCart(cartAdd.getAWSAccessKeyId()); //If user did not create a Cart for this session if(cart == null){ cartAddResponse.getCart().add(new Cart()); return cartAddResponse; } CartItem cartItem = new CartItem(); cartItem.setASIN(cartAddRequest.getItems().getItem().get(0).getASIN()); cartItem.setQuantity(cartAddRequest.getItems().getItem().get(0).getQuantity().toString()); cart.getCartItems().getCartItem().add(cartItem); cartAddResponse.getCart().add(cart); return cartAddResponse; } public CartGetResponse CartGet(CartGet cartGet) { CartGetResponse cartGetResponse = new CartGetResponse(); Cart cart = getCart(cartGet.getAWSAccessKeyId()); if(cart == null){ cartGetResponse.getCart().add(new Cart()); return cartGetResponse; } cartGetResponse.getCart().add(cart); return cartGetResponse; } public CartModifyResponse CartModify(CartModify cartModify) { //Implementation here... } public CartClearResponse CartClear(CartClear cartClear) { //Implementation here... } private synchronized String generateID(){ ID++; return String.valueOf(ID); } private Cart getCart(String usedId){ Cart cart = null; System.out.println(this.cartsHash.toString()); cart = this.cartsHash.get(cartId); return cart; } private void addCart(String userId, Cart cart){ this.cartsHash.put(cartId, cart); } }
It is worth highlighting the fact that this class was annotated with a @Scope("COMPOSITE") annotation. This annotation is important since component implementations can either manage their own state or allow the SCA runtime to do so. In the latter case, the implementation scope specifies a visibility and lifecycle contract an implementation has with the SCA runtime. Invocations on a service offered by a component will be dispatched by the SCA runtime to an implementation instance according to the semantics of its implementation scope. In this case, the @Scope("COMPOSITE") tells the runtime to dispatch all service requests to the same implementation instance for the lifetime of the containing composite. This way, we can be sure that we can maintain the session data for each user (i.e. retrieving the active cart and its items).
Shopping Cart SCA Composite
Now that our Shopping Cart service has been implemented, we need to integrate it into Tuscany. In order to do that we need to define a composite file; let us call it shoppingstore.composite.
<composite xmlns="http://www.osoa.org/xmlns/sca/1.0" targetNamespace="http://webservices.amazon.com/AWSECommerceService/2007-05-14" xmlns:tns="http://webservices.amazon.com/AWSECommerceService/2007-05-14" xmlns:db="http://tuscany.apache.org/xmlns/databinding/1.0" name="ShoppingStore"> <component name="CartServiceComponent"> <service name="CartService" > <interface.wsdl interface="http://webservices.amazon.com/AWSECommerceService/2007-05-14#wsdl.interface(AWSECommerceServicePortType)" /> <binding.ws /> <db:databinding name="jaxb" /> </service> <implementation.java class="shoppingstore.services.cart.CartServiceImpl" /> </component> </composite>
This composite file, called ShoppingStore, contains only one component, the CartServiceComponent, which is offering its service via a web service interface. It is worth mentioning that the name of the service being offered by the Cart component (i.e. CartService) must be equal to the Java interface of the cart service (i.e. CartService.java).
Preparing to Run the Service
Now that we have a Tuscany Service modeled and implemented we need to get it running. Therefore, what we are going to do is to use the embedded server Tuscany provides in its releases. Thus, let us code the Java class that will have the main method and will prepare the service to start "listening" for requests, let us call it ShoppingStoreServer.java:
package shoppingstore.server; import java.io.IOException; import org.apache.tuscany.sca.host.embedded.SCADomain; public class ShoppingStoreServer { public static void main(String[] args) { SCADomain scaDomain = SCADomain.newInstance("shoppingstore.composite"); try { System.out.println("ShoppingStore server started (press enter to shutdown)"); System.in.read(); } catch (IOException e) { e.printStackTrace(); } scaDomain.close(); System.out.println("ShoppingStore server stopped"); } }
The code above creates a new instance of the ShoppingStore composite and leaves it running awaiting for requests through its web service interface (as described in its composite file).
Running the Composite
We have already coded everything we need. Now, we need to properly set the environment to run the composite...
1 - Install Tuscany: Download it (http://cwiki.apache.org/TUSCANY/sca-java-releases.html) and uncompress it on the directory where you want it to be located (e.g. /opt/tuscany-sca-1.0-incubating).
2 - Compile the Shopping Store code (replace <TUSCANY_HOME> with the directory where Tuscany is located):
Windows:
#set TUSCANY_HOME=<TUSCANY_HOME> #javac .\src\shoppingstore\server\*.java .\src\shoppingstore\services\cart\*.java -cp .\lib\AWS2007_05_14.jar;%TUSCANY_HOME%\lib\tuscany-sca-manifest.jar -d build
Linux:
#TUSCANY_HOME="<TUSCANY_HOME>" #javac ./src/shoppingstore/server/*.java ./src/shoppingstore/services/cart/*.java -cp ./lib/AWS2007_05_14.jar:$TUSCANY_HOME/lib/tuscany-sca-manifest.jar -d build
3 - Create and arrange the required jar files: in order to run the Shopping Store composite, we first need to generate the required jar files. Let us begin:
3.1 - Create shoppingstore.jar: this file must contain the compile code for the application, its composite file and the reduced version of the Amazon WSDL. We will generate the jar file out of the files contained in the build directory since it already has the files in the structure we need them to be. Run:
Windows:
#jar cvfM .\dist\shoppingstore.jar -C .\build .
Linux:
#jar cvfM ./dist/shoppingstore.jar -C ./build .
3.2 - Now, copy the lib directory (along with the contained AWS2007_05_14.jar file) into the "dist" directory. It must look like this:
-dist
- lib
AWS2007_05_14.jar
shoppingstore.jar
4 - Run the service: In order to run the service, run the following command:
Windows:
#set TUSCANY_HOME=<TUSCANY_HOME> #java -cp %TUSCANY_HOME%\lib\tuscany-sca-manifest.jar;.\dist\shoppingstore.jar;.\dist\lib\AWS2007_05_14.jar shoppingstore.server.ShoppingStoreServer
Linux:
#TUSCANY_HOME="<TUSCANY_HOME>" #java -cp $TUSCANY_HOME/lib/tuscany-sca-manifest.jar:./dist/shoppingstore.jar:./dist/lib/AWS2007_05_14.jar shoppingstore.server.ShoppingStoreServer
5 - Test the service: Instead of writing a standalone Java client which will invoke our web service, we can test it is working correctly in a much easier way. Let us just create a telnet connection to the port where Tuscany is listening for requests and let us send a SOAP message over it. So, the steps would be:
5.1 Execute
#telnet <HOSTNAME> <PORT> (e.g. #telnet localhost 8080)
and a connection to Tuscany's web server will be established.
5.2 Now, paste the following text into the terminal in order to create a Cart:
POST /CartServiceComponent HTTP/1.0 Accept-Language: en Content-Type: text/xml; charset="utf-8" Content-Id: soap-envelope Content-Length: 328 SOAPAction: http://soap.amazon.com <?xml version="1.0" encoding="UTF-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Body> <CartCreate xmlns="http://webservices.amazon.com/AWSECommerceService/2007-05-14"> <AWSAccessKeyId>112233</AWSAccessKeyId> <Request/> </CartCreate> </soapenv:Body> </soapenv:Envelope>
There are only two relevant things the reader needs to understand about this XML text:
1 - The operation being executed is denoted by the tag "CartCreate"
2 - There is only one parameter this operation receives, the AWSAccessKeyId (which univocally identifies an Amazon user) value (equal to 112233 in this case)
After a few moments of pasting the XML text, the Cart service will respond with an XML where the ID of the Cart assigned to the user is shown (just for debugging purposes). It is an incremental value, so the first user will get CartId = 1, the second client to create a Cart will be assigned the Cart with ID number 2, an so on.
The Cart service running within Tuscany displays the activity being carried out upon each request, this information can be useful for the readers to double check that the service is properly processing the requests being sent.
After creating the Cart, Items can be added to it. The following message does that:
POST /CartServiceComponent HTTP/1.0 Accept-Language: en Content-Type: text/xml; charset="utf-8" Content-Id: soap-envelope Content-Length: 428 SOAPAction: http://soap.amazon.com <?xml version="1.0" encoding="UTF-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Body> <CartAdd xmlns="http://webservices.amazon.com/AWSECommerceService/2007-05-14"> <AWSAccessKeyId>112233</AWSAccessKeyId> <Request> <Items> <Item> <ASIN>Item001</ASIN> <Quantity>2</Quantity> </Item> </Items> </Request> </CartAdd> </soapenv:Body> </soapenv:Envelope>
Let us try to understand this XML file:
1 - The operation being executed is denoted by the tag "CartAdd"
2 - The AWSAccessKeyId must have associated a Cart to it (i.e. a CartCreate operation has been executed for this same user)
3 - The Item is composed of ASIN (Amazon's ID for Items) and the quantity of those items to be added to the cart.
Again, the Cart service will respond with a XML text showing that the item has being received and processed.
Conclusions
What we have learnt after reading this document is to create a Tuscany application out of a WSDL file. Following this top-down approach is the natural way of modeling Tuscany applications, and is very convenient when the WSDL file is already available.
Throughout this document, all the steps required to have a running Tuscany service were detailed. However, it is important to point that although we reached to a application offering its service via a WS interface, we could have exposed it via other interfaces and transports since Tuscany offers such intrinsic flexibility.
Even though we came across some inconveniences when coding the application which led us to write this document, Tuscany proved to be a good choice to model and implement a service-oriented application following a top down approach.