Raible's Wiki
Raible Designs AppFuseHomepage- Korean - Chinese - Italian - Japanese QuickStart Guide User Guide Tutorials Other ApplicationsStruts ResumeSecurity Example Struts Menu
Set your name in
UserPreferences
Referenced by
JSPWiki v2.2.33
Hide Menu |
Webservices out of AppFuse - A HowTo for providing Webservices from within AppFuse. About this TutorialThis tutorial will show you how to create a Webservice and what you can do to automate the creation of artifacts necessary to provide Webservices to others.Table of Contents
Preface [#0]You may wonder, why I bother to write a howto for XFire, given that there is already one for Axis, the far more widely used Webservice framework. But you may wonder as well, why the codehaus people bother writing XFire at all, years after Axis was published in the first place. The reason for the latter, in my personal opinion, is that Axis 1 has some shortfalls. Namely dealing with Collections and it is not so easy to plug in another XML binding framework. Why should you not be comfortable with the built in XML binding? Because it is not as powerful as for example XmlBeans. You have to limit your application to a subset of what's possible with webservices. Another, in some cases important benefit is performance (in terms of speed), that is because the next generation webservice frameworks (like Axis 2 and XFire) are based an StAX, the Streaming Api for Xml. Apache is aware of this limitations of Axis 1 and has taken Axis 2 on board as a replacement build from scratch for Axis 1. Axis 2 is (as well as XFire) in pre 1.0 state, XFire is, however, just a few days away from it's 1.0 as of Feb 2006. My motivation to use XFire instead of Axis 2 is, that I started using it, was successful in short time, and just stuck with it, becoming a committer of the project.Install the necessary artifacts from the extras package [#1]Since AppFuse 1.9.1 there is an extras package named xfire which enables Webservice with xfire in AppFuse. Go into extras/xfire and issueant installthere. This install everything you need to create your own webservices, but doesn't touch your code. In detail it does the following:
To test, if everything worked as expected you can issue ant testin the extras directory. It should execute the EchoWebserviceTest, which creates a webservice using spring, get's the wsdl and sends a message to the webservice. This test is really simple in it's assemblage but does a powerful test of the whole webservice stack: package org.appfuse.service; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.xfire.spring.AbstractXFireSpringTest; import org.jdom.Document; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class EchoWebServiceTest extends AbstractXFireSpringTest { protected final Log log = LogFactory.getLog(getClass()); public void setUp() throws Exception { super.setUp(); } public void testGetWsdl() throws Exception { Document doc = getWSDLDocument("Echo"); printNode(doc); assertValid("//xsd:element[@name=\"echo\"]", doc); assertValid("//xsd:element[@name=\"echoResponse\"]", doc); } public void testCallEcho() throws Exception { Document response = invokeService("Echo", "/org/appfuse/service/echo.xml"); printNode(response); addNamespace("service","http://test.xfire.codehaus.org"); assertValid("//service:echoResponse/service:out[text()=\"Hello world!\"]",response); } protected ApplicationContext createContext() { return new ClassPathXmlApplicationContext(new String[]{ "org/appfuse/service/applicationContext-test.xml", "org/appfuse/service/applicationContext-webservice.xml"}); } } invokeService() for example uses the LocalBinding of XFire to send the echo.xml file into the stack and assertValid() uses XPath expressions to examine the (xml)response of the service. echo.xml <env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"> <env:Header/> <env:Body> <echo xmlns="http://service.appfuse.org"> <in0>Hello world!</in0> </echo> </env:Body> </env:Envelope> You'll find more about testing in section [3]. The UserWebservice demo inspected [#2]The extras package contains a demo showing how to create webservices out of AppFuse. You can install the demo by issuing ant install-demoin the extras/appfuse directory. Installing the demo actually means adding xdoclet tags to User.java, Role.java in the model and adding an additional interface to UserManagerImpl.java in service/impl. The additional interface is: package org.appfuse.service; import java.util.List; import org.appfuse.dao.UserDAO; import org.appfuse.model.User; /** * WebService Interface * * <p><a href="UserWebService.java.html"><i>View Source</i></a></p> * * @author <a href="mailto:[email protected]">Mika Goeckel</a> * @aegis.mapping */ public interface UserWebService { //~ Methods ================================================================ /** * Retrieves a user by username. An exception is thrown if now user * is found. * * @param username * @return User */ public User getUser(String username); /** * Retrieves a list of users, filtering with parameters on a user object * @param user parameters to filter on * @return List * * Tell xfire, which objects it will find in the List * @aegis.method * @aegis.method-return-type componentType="org.appfuse.model.User" */ public List getUsers(User user); /** * Saves a user's information * * @param user the user's information * @throws UserExistsException */ public void saveUser(User user) throws UserExistsException; /** * Removes a user from the database by their username * * @param username the user's username */ public void removeUser(String username); } The rationale behind the additional interface is to clearly separate the methods that you want to expose as webservice from these that you use inside your application. Actually there is no way to ignore a method of an exposed interface in xfire as of version 1.0 like the @xfire.property ignore="true" that exists on the model objects (we'll com to that later), so having a second interface is probably the best way to distinct at the moment. Looking on the source code of the interface above, you noticed the uncommon xdoclet tags @aegis.mapping at class level and @aegis.method and @aegis.method-return-type on the getUsers() method. These are tags which are processed using the aegis-mapping.xdt xdoclet template which was added earlier into metadata/templates by the ant install task. XFire has several ways of describing the metainformation you need to create a webservice. One of them is aegis, another is JSR181-Annotations. Aegis works with xml files which contain the metadata. These files look like <?xml version="1.0" encoding="utf-8"?> <mappings> <mapping> <method name="getUsers"> <return-type componentType="org.appfuse.model.User"/> </method> </mapping> </mappings>in fact the file above was created by the described xdoclet task out of the annotations that were mentioned before. In this case the tag describes to xfire what objects it should expect in the List, the getUsers() method returns. This allows xfire to assign the right return type to the method in the wsdl (webservice description language) which is <xsd:complexType name="ArrayOfUser"> <xsd:sequence> <xsd:element name="User" type="ns1:User" nillable="true" minOccurs="0" maxOccurs="unbounded" /> </xsd:sequence> </xsd:complexType>instead of ArrayOfAnyType which would be generated if we wouldn't give that hint to xfire. Exactly that problem makes it necessary to add some xdoclet tags to the model we use in AppFuse. User references a List of Roles and Role backreferences User (later more on that). So the getRoles() method on the User object needs a tag as well: /** * @hibernate.set table="user_role" cascade="save-update" lazy="false" * @hibernate.collection-key column="username" * @hibernate.collection-many-to-many class="org.appfuse.model.Role" column="role_name" * @aegis.property componentType="org.appfuse.model.Role" */ public Set getRoles() { return roles; }Here it is the aegis.property instead of aegis.method-return-type. Don't forget the class level @aegis.mapping tag which serves as a marker for the xdoclet ant task, omitting it is a common mistake I made during the creation of this howto :-) So now more on the backreference in Role.java: /** * @return Returns the users. * This inverse relation causes exceptions :-( drk * hibernate.set table="user_role" cascade="save-update" * lazy="false" inverse="true" * hibernate.collection-key column="role_name" * hibernate.collection-many-to-many class="org.appfuse.model.User" * column="username" * @aegis.property ignore="true" */ public Set getUsers() { return users; }why do we ignore this property instead of telling XFire to create an ArrayOfRole like we did above? It's because we need to avoid circular references. If we had a User referencing a Role referencing the same User referencig the same Role, we get a StackOverflowException once we call the service. This is because xfire is all about document/literal style of webservices which just has no notion of object references because of interoperability issues. You can read more about different styles of webservices here. So our only option is to ignore the backreference. This requires return values to be trees of objects. What else do you need to do to expose something as a webservice. We already had the aegis xdoclet annotations, which are only necessary in case you have Collection types, but how does xfire know what service you want to expose? XFire integrates with several IoC frameworks, PicoContainer, Plexus, Loom and last but definitiely not least Spring. Besides that XFire can be configured through XFireConfigurableServlet and XFireServlet. You even don't need a servlet container, because XFire comes with a built in Jetty so that you could run completely stand alone. In AppFuse we've got Spring and we run inside Tomcat or another ServletContainer (XFire has proven to run in Tomcat, JBoss and Weblogic in my tests, other probably work as well), so we use Spring to tell XFire what to do. You might have notices the applicationContext-webservice.xml which we copied into the src/service/org/appfuse/service directory: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <import resource="classpath:org/codehaus/xfire/spring/xfire.xml"/> <bean name="echoService" class="org.codehaus.xfire.spring.ServiceBean"> <property name="serviceBean" ref="echo"/> <property name="serviceClass" value="org.codehaus.xfire.test.Echo"/> <property name="inHandlers"> <list> <ref bean="addressingHandler"/> </list> </property> </bean> <bean id="echo" class="org.codehaus.xfire.test.EchoImpl"/> <bean name="userService" class="org.codehaus.xfire.spring.ServiceBean"> <property name="serviceBean" ref="userManager"/> <property name="serviceClass" value="org.appfuse.service.UserWebService"/> <property name="inHandlers"> <list> <ref bean="addressingHandler"/> </list> </property> </bean> <bean id="addressingHandler" class="org.codehaus.xfire.addressing.AddressingInHandler"/> </beans> I chose this way, because I find it easy to understand and to use, given the structure of AppFuse. So such xml Spring beans files are not uncommon to you. The import tag at the beginning is our way to include the standard xfire bean definitions (have a look into that file, it's in xfire-spring.jar) We use the ServiceBean from xfire-spring.jar, which is easy to configure. If you ignore the AddressingInHandler (which is to support ws-addressing header information), it's really straight forward. You define an interface (property serviceClass) which will be exposed and an implementation (in this case a reference to userManager, defined in applicationContext-service.xml). The first definition (echoService) is a simple test service which just returns the string you put in as argument (the one we called in EchoWebServiceTest above). So to create additional services, you don't need more than add another ServiceBean with your interface and implementation class. ServiceBean is more complex than that snippet of xml suggests. You can control large portions of the behavior of xfire using your impementations of other properties ServiceBean takes. The complete processing stack (In-, Out- and Fault-Handlers), ServiceFactory, known XML-Schemas, Bindings, Scope (request, session, application) and much more. Through controlling the ServiceFactory you even got more options. But the default settings is just what we need here. Unfortunately the best way to understand the possibilities is reading the code, as there is not much documentation there right now. But be assured, the code is really understandable. If you want to see your service live, do an ant deploy and go to http://localhost:8080/appfuse/services with your browser. You should see Services: * Echo * UserWebService Next, you can get the wsdl of these services by calling http://localhost:8080/appfuse/services/UserWebService?wsdl which should produce some lines of xml, something like <?xml version="1.0" encoding="UTF-8"?> <wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:ns1="http://model.appfuse.org" xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope" xmlns:soapenc11="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soapenc12="http://www.w3.org/2003/05/soap-encoding" xmlns:tns="http://service.appfuse.org" xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://service.appfuse.org"> <wsdl:types xmlns:ns2="http://acegisecurity.org"> <xsd:schema targetNamespace="http://model.appfuse.org" elementFormDefault="qualified" attributeFormDefault="qualified"> <xsd:complexType name="User"> <xsd:sequence> <xsd:element name="accountExpired" type="xsd:boolean" minOccurs="0" /> <xsd:element name="accountLocked" type="xsd:boolean" minOccurs="0" /> <xsd:element name="accountNonExpired" type="xsd:boolean" minOccurs="0" /> <xsd:element name="accountNonLocked" type="xsd:boolean" minOccurs="0" /> ... Testing without container [#3]Unfortunately you can't call the xfire webservice like you can with axis by just appending parameters like form parameters yet. Thatis a feature which is currently under development. But you can test your service with eclipse-wtp, there you find under Run->Launch the Webservices Explorer a mighty tool to test your services. It's a little bit tricky to get to the service using the explorer, because you have to know that you need to click on the secondmost icon in the right upper corner of the window (WSDL page hoover). If you click there you get a "WSDL Main" entry in the Navigator box on the left hand, click there and you get a input box for your wsdl url on the right side. There you enter http://localhost:8080/appfuse/services/UserWebService?wsdl and the explorer loads your wsdl and shows the methods that your interface exposes. Click on "getUsers" and you see select boxes for all properties of the input User object you can use to select the users you'd like to be displayed. For the time just klick on the "Go" button at the very end of that list and you'll get a list of the users currently registered in your database.This is an easy, but unfortunately very manual way of testing your service. As a test infected AppFuse developer you probably want to use unit tests for your services. We've seen a simple testcase above (EchoWebServiceTest) now we want to dive deeper into testing of webservices with the UserWebServiceTest: package org.appfuse.service; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.appfuse.dao.UserDAO; import org.appfuse.model.User; import org.codehaus.xfire.spring.AbstractXFireSpringTest; import org.jdom.Document; import org.jmock.Mock; import org.jmock.core.constraint.IsEqual; import org.jmock.core.matcher.InvokeOnceMatcher; import org.jmock.core.stub.ReturnStub; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class UserWebServiceTest extends AbstractXFireSpringTest { protected final Log log = LogFactory.getLog(getClass()); public void setUp() throws Exception { super.setUp(); } public void testGetWsdl() throws Exception { Document doc = getWSDLDocument("UserWebService"); printNode(doc); assertValid("//xsd:complexType[@name=\"User\"]", doc); assertValid("//xsd:complexType[@name=\"Role\"]", doc); } public void testGetUser() throws Exception { // Setup testharness User testData = new User("tomcat"); testData.setEnabled(true); Mock userDAO = new Mock(UserDAO.class); // because we can't extend MockObjectTestCase we create new instances for once(), eq() and returnValue() InvokeOnceMatcher once = new InvokeOnceMatcher(); IsEqual eq = new IsEqual("tomcat"); ReturnStub returnValue = new ReturnStub(testData); userDAO.expects(once).method("getUser").with(eq).will(returnValue); UserManager service = (UserManager) getContext().getBean("userManager"); service.setUserDAO((UserDAO)userDAO.proxy()); // invoke webservice Document response = invokeService("UserWebService", "/org/appfuse/service/getUser.xml"); //printNode(response); // verify result userDAO.verify(); addNamespace("service","http://service.appfuse.org"); addNamespace("model","http://model.appfuse.org"); assertValid("//service:getUserResponse/service:out[model:username=\"tomcat\"]",response); assertValid("//service:getUserResponse/service:out[model:enabled=\"true\"]",response); } protected ApplicationContext createContext() { return new ClassPathXmlApplicationContext(new String[]{ "org/appfuse/service/applicationContext-test.xml", "org/appfuse/service/applicationContext-webservice.xml"}); } } The testGetUser shows that the test really calls through to your Manager class. It shows as well how you can use JMock even if you don't extend JMock's MockObjectTestCase which you would do if you use the BaseManagerTestCase of AppFuse. Because we need the functionality of the Spring/XFire TestCase classes, we can't use the BaseManagerTestCase but fortunately there is a way around. I hope I could explain how to use XFire together with AppFuse a little bit. Once you started playing arounf with XFire you probably find out more and more details that aren't even mentioned in this HowTo, please feel free to extend it. The former tutorial is available here
|