Thursday, February 19, 2009

Exposing Tables Mapped to Entity Beans as a Webservice

I wanted to create a simple web service to access some database tables. This turns out to be a little bit harder than I though as the classes generated for JPA are a little bit different in intention as those required JAXB / Web Services which are more method driven. This example just uses EJB; but the same should apply to using JPA straight.

Although the screen grabs are from JDeveloper, of course, the the following should apply to most java IDEs. So first of all lets create some entities from tables in the database:

Then I am going to create a connection to the old favourite scott/tiger:

Now in the screen shot and a few more I have selected more than just Emp/Dept. Just ignore that and assume that I had just selected the two. I didn't have the heart to go back and re-shoot them.

So finish the entity bean wizard and then bring up the EJB Sesssion Bean wizard so we can create a suitable Façade to publish as our web service.

You can pretty much accept the defaults on this page as it has already picked up the persistence unit from the previous wizard:

Now we can just clagg on the @WebService annotation and run the service....

... well it turns out that if you try to run the Session Bean that it fails to deploy to the server with something like the following error:


[HTTP:101216]Servlet: "WSEE_SERVLET" failed to preload on startup in Web application: "/EmpDept-EmpDeptFacade-webapp".
javax.xml.ws.WebServiceException: Unable to create JAXBContext
 at com.sun.xml.ws.model.AbstractSEIModelImpl.createJAXBContext(AbstractSEIModelImpl.java:158)
 ...
 at weblogic.work.ExecuteThread.run(ExecuteThread.java:173)
Caused by: java.security.PrivilegedActionException: com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
java.sql.Timestamp does not have a no-arg default constructor.
 this problem is related to the following location:
  at java.sql.Timestamp
  at public java.sql.Timestamp empdeptfacade.Emp.getHiredate()
  at empdeptfacade.Emp
  at public java.util.List empdeptfacade.jaxws.QueryEmpFindAllResponse._return
  at empdeptfacade.jaxws.QueryEmpFindAllResponse

 at java.security.AccessController.doPrivileged(Native Method)
 at com.sun.xml.ws.model.AbstractSEIModelImpl.createJAXBContext(AbstractSEIModelImpl.java:148)
 ... 54 more
Caused by: com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
java.sql.Timestamp does not have a no-arg default constructor.
 this problem is related to the following location:
  at java.sql.Timestamp
  at public java.sql.Timestamp empdeptfacade.Emp.getHiredate()
  at empdeptfacade.Emp
  at public java.util.List empdeptfacade.jaxws.QueryEmpFindAllResponse._return
  at empdeptfacade.jaxws.QueryEmpFindAllResponse

 at com.sun.xml.bind.v2.runtime.IllegalAnnotationsException$Builder.check(IllegalAnnotationsException.java:102)
 at com.sun.xml.bind.v2.runtime.JAXBContextImpl.getTypeInfoSet(JAXBContextImpl.java:438)
 at com.sun.xml.bind.v2.runtime.JAXBContextImpl.(JAXBContextImpl.java:286)
 at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:139)
 at com.sun.xml.bind.api.JAXBRIContext.newInstance(JAXBRIContext.java:105)
 at com.sun.xml.ws.model.AbstractSEIModelImpl$1.run(AbstractSEIModelImpl.java:153)
 at com.sun.xml.ws.model.AbstractSEIModelImpl$1.run(AbstractSEIModelImpl.java:148)
 ... 56 more

Now JAXB will handle quite a few cases/classes by default; but one annoying case it won't deal with is the java.sql.Timestamp class as it doesn't have a no-arg constructor. So work around this you need to define an adapter to do the conversion, here is a trivial implementation that may not work in all cases, particularly is you plan to write back to the database:

package empdeptfacade;

import java.sql.Timestamp;

import java.util.Date;

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class TimestampAdapter
  extends XmlAdapter<Date, Timestamp> {

    public Date marshal(Timestamp v) {
        return new Date(v.getTime());
    }

    public Timestamp unmarshal(Date v) {
        return new Timestamp(
            v.getTime());
    }
}

Now you could attach this to each and every usage of the Timestamp class, admittedly just the once in this case, but a much tidier way to do it is to attach it to the package-info.java class instead and have it apply to a number of classes. Unfortunately JDeveloper doesn't allow you to create one from the java wizard - so you just have to use the "New File" wizard from the gallery.

And the code for the package needs to look like:


@XmlJavaTypeAdapters(
   @XmlJavaTypeAdapter(value=TimestampAdapter.class,type=Timestamp.class))
package empdeptfacade;

import java.sql.Timestamp;

import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;

You should now find that your EJB will deploy quite happily; but you will find that if you actually invoke one of the methods on the web service it fails with the following error:

avax.xml.ws.WebServiceException: javax.xml.bind.MarshalException
 - with linked exception:
[com.sun.istack.SAXException2: A cycle is detected in the object graph. This will cause infinitely deep XML: empdeptfacade.Dept@50dd8d -> empdeptfacade.Emp@11541d8 -> empdeptfacade.Dept@50dd8d]
 at com.sun.xml.ws.message.jaxb.JAXBMessage.writePayloadTo(JAXBMessage.java:322)
 at com.sun.xml.ws.message.AbstractMessageImpl.writeTo(AbstractMessageImpl.java:153)
 at com.sun.xml.ws.encoding.StreamSOAPCodec.encode(StreamSOAPCodec.java:108)
 at com.sun.xml.ws.encoding.SOAPBindingCodec.encode(SOAPBindingCodec.java:258)
 at com.sun.xml.ws.transport.http.HttpAdapter.encodePacket(HttpAdapter.java:326)
 at com.sun.xml.ws.transport.http.HttpAdapter.access$100(HttpAdapter.java:99)
 at com.sun.xml.ws.transport.http.HttpAdapter$HttpToolkit.handle(HttpAdapter.java:460)
 at com.sun.xml.ws.transport.http.HttpAdapter.handle(HttpAdapter.java:250)
 at com.sun.xml.ws.transport.http.servlet.ServletAdapter.handle(ServletAdapter.java:140)
 at weblogic.wsee.jaxws.HttpServletAdapter$AuthorizedInvoke.run(HttpServletAdapter.java:289)
 at weblogic.wsee.jaxws.HttpServletAdapter.post(HttpServletAdapter.java:202)
 at weblogic.wsee.jaxws.JAXWSServlet.doPost(JAXWSServlet.java:237)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
 at weblogic.wsee.jaxws.JAXWSServlet.service(JAXWSServlet.java:77)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
 at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:227)
 at weblogic.servlet.internal.StubSecurityHelper.invokeServlet(StubSecurityHelper.java:125)
 at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:292)
 at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:175)
 at weblogic.servlet.internal.WebAppServletContext$ServletInvocationAction.run(WebAppServletContext.java:3586)
 at weblogic.security.acl.internal.AuthenticatedSubject.doAs(AuthenticatedSubject.java:321)
 at weblogic.security.service.SecurityManager.runAs(SecurityManager.java:121)
 at weblogic.servlet.internal.WebAppServletContext.securedExecute(WebAppServletContext.java:2196)
 at weblogic.servlet.internal.WebAppServletContext.execute(WebAppServletContext.java:2102)
 at weblogic.servlet.internal.ServletRequestImpl.run(ServletRequestImpl.java:1428)
 at weblogic.work.ExecuteThread.execute(ExecuteThread.java:201)
 at weblogic.work.ExecuteThread.run(ExecuteThread.java:173)
Caused by: javax.xml.bind.MarshalException
 - with linked exception:
[com.sun.istack.SAXException2: A cycle is detected in the object graph. This will cause infinitely deep XML: empdeptfacade.Dept@50dd8d -> empdeptfacade.Emp@11541d8 -> empdeptfacade.Dept@50dd8d]
 at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:282)
 at com.sun.xml.bind.v2.runtime.BridgeImpl.marshal(BridgeImpl.java:90)
 at com.sun.xml.bind.api.Bridge.marshal(Bridge.java:107)
 at com.sun.xml.ws.message.jaxb.JAXBMessage.writePayloadTo(JAXBMessage.java:317)
 ... 26 more
Caused by: com.sun.istack.SAXException2: A cycle is detected in the object graph. This will cause infinitely deep XML: empdeptfacade.Dept@50dd8d -> empdeptfacade.Emp@11541d8 -> empdeptfacade.Dept@50dd8d
 at com.sun.xml.bind.v2.runtime.XMLSerializer.reportError(XMLSerializer.java:244)
 at com.sun.xml.bind.v2.runtime.XMLSerializer.pushObject(XMLSerializer.java:533)
 at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:627)
 at com.sun.xml.bind.v2.runtime.property.SingleElementNodeProperty.serializeBody(SingleElementNodeProperty.java:150)
 at com.sun.xml.bind.v2.runtime.ClassBeanInfoImpl.serializeBody(ClassBeanInfoImpl.java:322)
 at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:681)
 at com.sun.xml.bind.v2.runtime.property.ArrayElementNodeProperty.serializeItem(ArrayElementNodeProperty.java:65)
 at com.sun.xml.bind.v2.runtime.property.ArrayElementProperty.serializeListBody(ArrayElementProperty.java:168)
 at com.sun.xml.bind.v2.runtime.property.ArrayERProperty.serializeBody(ArrayERProperty.java:152)
 at com.sun.xml.bind.v2.runtime.ClassBeanInfoImpl.serializeBody(ClassBeanInfoImpl.java:322)
 at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:681)
 at com.sun.xml.bind.v2.runtime.property.ArrayElementNodeProperty.serializeItem(ArrayElementNodeProperty.java:65)
 at com.sun.xml.bind.v2.runtime.property.ArrayElementProperty.serializeListBody(ArrayElementProperty.java:168)
 at com.sun.xml.bind.v2.runtime.property.ArrayERProperty.serializeBody(ArrayERProperty.java:152)
 at com.sun.xml.bind.v2.runtime.ClassBeanInfoImpl.serializeBody(ClassBeanInfoImpl.java:322)
 at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:681)
 at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:277)
 ... 29 more

Problem here is the methods it generates are a bit too helpful. For example Dept has a method that returns all of the Employees in the Department which in turn returns the Department you are in. Now there are a variety of ways you can solve this problem but I am going for the simplest method of marking the employee accessors of Dept as transient.

You can now run the service and get a list of departments back:

Now the Emp class has a similar problem in that for each Emp you get a copy of the Dept for it. This denormlizes the data and might be a problem in certain use cases. You can consider using XMLID/XMLIDref as mentioned in the previous link but this will only work properly if you return a structure that contains the employees and the department. Otherwise you will not get any department objects back to the client. You would need to make the following changes to get this to work:


// Changes to Dept.java note XmlID has to be of type String
// otherwise JAXB will complain.

    @XmlID
    @XmlJavaTypeAdapter(LongAdapter.class)
    public Long getDeptno() {
        return deptno;
    }

// Changes to Empt.java

    @XmlIDREF
    public Dept getDept() {
        return dept;
    }

// Long Adapter

public class LongAdapter extends XmlAdapter<String, Long> {


    public Long unmarshal(String v) {
        return Long.parseLong(v);
    }

    public String marshal(Long v) {
        return v.toString();
    }
}

// Extra query method in SessionEJBBean

    public EmpDeptList queryEmpFindAllWithDepts() {
        List<Emp> list = em.createNamedQuery("Emp.findAll").getResultList();
        Set<Dept> deps = new HashSet<Dept>();
        for (Emp emp : list) {
            deps.add(emp.getDept());
        }
        
        EmpDeptList edl = new EmpDeptList();
        edl.setEmployees(list);
        edl.setDepartments(deps);
        return edl;
    }

The cool thing about JAX-B is that on the client side the object are linked back together even though in the XML message they are sent as two different lists. (Note the cast in Dept, not sure if this is a bug or a feature of using IDRef. Something to look into at a future date)

    sessionEJBBeanService = new SessionEJBBeanService();
    SessionEJBBean sessionEJBBean = sessionEJBBeanService.getSessionEJBBeanPort();
    // Add your code to call the desired methods.

    QueryEmpFindAllWithDeptsResponse allWithDepts =
        sessionEJBBean.queryEmpFindAllWithDepts(new QueryEmpFindAllWithDepts());
    Emp e = allWithDepts.getReturn().getEmployees().get(0);
    Dept d = (Dept)e.getDept();
    System.out.println(d.getDname());

Of course this exposes the limitation of the annotation programming model as it is much harder to present different XML views of your data depending with the same bean model. The key take away point I guess is that database data can be in the form or a mesh whereas you have to be careful when creating web services and generating XML that the data is mapped into a tree.

3 comments:

Unknown said...

How can I solve this problems using Jdeveloper 10.1.3.4.?

Unknown said...
This comment has been removed by a blog administrator.
Gerard Davison said...

Иван,

I am not enough of an expert in JAX-RPC to comment to be honest.

Gerard