Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CXF Restful service complex object unmarshalling does not work

Tags:

java

jaxb

cxf

I am new at restful services, and had a relatively good start, until I decided to play with some complex objects. The problem I am hitting is about unmarshalling an object coming to the server (creating object from XML on the server side).

Below is my sample (representative) implementation of the service.

Here is my "complex object" data type.

package data;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class ComplexType {
    private long id;
    private String name;
    private Boolean isRegistered;


    public ComplexType() {
        super();
    }
    public ComplexType(long id, String name, Boolean isRegistered) {
        super();
        this.id = id;
        this.name = name;
        this.isRegistered = isRegistered;
    }
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Boolean getIsRegistered() {
        return isRegistered;
    }
    public void setIsRegistered(Boolean isRegistered) {
        this.isRegistered = isRegistered;
    }
}

This is my service API

package api;

import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;

import data.ComplexType;


public interface Service {
    @GET
    @Path("/nummembers")
    int getNumElements();

    @GET
    @Path("/member/{id}")
    ComplexType getMember(@PathParam("id") long id);

    @POST
    @Path("/member")
    boolean addMember(@FormParam("member") ComplexType member);
}

And this is the implementation of the service:

package impl;

import java.util.HashMap;
import java.util.Map;

import data.ComplexType;
import api.Service;

public class ServiceImpl implements Service {
    Map<Long, ComplexType> data;

    public ServiceImpl() {
        System.out.println("TestApp Starting");
        data = new HashMap<Long, ComplexType>();
    }

    @Override
    public int getNumElements() {
        return data.size();
    }

    @Override
    public ComplexType getMember(long id) {
        if (data.containsKey(id)) {
            return data.get(id);
        }
        ComplexType ct =
            new ComplexType(id, "NAME" + new Long(id).toString(), (id % 2 == 1));
        data.put(id, ct);
        return ct;
    }

    @Override
    public boolean addMember(ComplexType member) {
        int preSize = data.size();
        data.put(member.getId(), member);
        return preSize < data.size(); // True if added
    }
}

So, when I call getNumElements() there is no problem. When I call getMember(long id) I get a serialized "ComplexType" just fine. When I serialize a complex type and pass it as FormParam to addMember(ComplexType member), I always get Parameter Class data.ComplexType has no constructor with single String parameter, static valueOf(String) or fromString(String) methods

I tried to provide my custom provider, by writing the following class:

package impl;

import javax.ws.rs.ext.ContextResolver;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;

import data.ComplexType;

public class JaxbXmlContextResolver implements ContextResolver<Object> {
    private static final Class<?>[] classes = new Class[] {ComplexType.class};
    private static final JAXBContext context = initContext();

    public static JAXBContext initContext() {
        JAXBContext context = null;
        try {
            context = JAXBContext.newInstance(classes);
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
        return context;
    }

    @Override
    public Object getContext(Class<?> arg0) {
        return context;
    }
}

And for the rest of the configurations, here is my web.xml:

<?xml version="1.0"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
  <display-name>TestApp</display-name>

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:cxf.xml</param-value>
  </context-param>

  <context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>classpath:log4j.properties</param-value>
  </context-param>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <servlet>
    <servlet-name>CXFServlet</servlet-name>
    <display-name>CXF Servlet</display-name>
    <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>CXFServlet</servlet-name>
    <url-pattern>/services/*</url-pattern>
  </servlet-mapping>
</web-app>

And the cxf.xml it refers to:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:jaxrs="http://cxf.apache.org/jaxrs"
       xmlns:jaxws="http://cxf.apache.org/jaxws"
       xmlns:cxf="http://cxf.apache.org/core"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/util
                           http://www.springframework.org/schema/util/spring-util-2.0.xsd
                           http://cxf.apache.org/jaxrs
                           http://cxf.apache.org/schemas/jaxrs.xsd">

  <import resource="classpath:META-INF/cxf/cxf.xml" />
  <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

  <bean id="myservice" class="impl.ServiceImpl" />
  <bean id="jaxbXmlProvider" class="impl.JaxbXmlContextResolver" />

  <jaxrs:server id="connectionService" address="/" >
    <jaxrs:serviceBeans>
      <ref bean="myservice" />
    </jaxrs:serviceBeans>
    <jaxrs:extensionMappings>
      <entry key="xml" value="application/xml" />
    </jaxrs:extensionMappings>
    <jaxrs:providers>
      <ref bean="jaxbXmlProvider" />
    </jaxrs:providers>
  </jaxrs:server>
</beans>

And for completeness, here is the pom.xml I am using to build the app.

<project xmlns="http://maven.apache.org/POM/4.0.0"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <modelVersion>4.0.0</modelVersion>
  <groupId>TESTAPP</groupId>
  <artifactId>testApp</artifactId>
  <packaging>war</packaging>
  <version>1.0</version>
  <name>Test Application</name>
  <url>http://www.mycompany.com</url>

  <dependencies>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.16</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.0.5.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.apache.cxf</groupId>
      <artifactId>cxf-rt-frontend-jaxrs</artifactId>
      <version>2.4.1</version>
      <exclusions>
        <exclusion>
          <groupId>wsdl4j</groupId>
          <artifactId>wsdl4j</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>com.thoughtworks.xstream</groupId>
      <artifactId>xstream</artifactId>
      <version>1.3.1</version>
    </dependency>
    <dependency>
      <groupId>net.sf.kxml</groupId>
      <artifactId>kxml2</artifactId>
      <version>2.2.2</version>
    </dependency>
    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.2.1</version>
    </dependency>

  </dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Any help (solutions, pointers, or any kind of direction) will be greatly appreciated.

Oh, I am using cxf 2.4.1 and Spring 3.0.5.RELEASE. This is exact copy of my deployed application.

Thanks.

like image 651
Virtually Real Avatar asked Aug 25 '11 19:08

Virtually Real


1 Answers

As it was stated at http://cxf.547215.n5.nabble.com/MessageBodyReader-not-picked-up-td564496.html, it turned out I needed a ParameterHandler. Since I had a large set of objects in my application, I did not want to create separate ParameterHandler<> for each, so I made a small change:

Using the technique described at JAXB inheritance, unmarshal to subclass of marshaled class, I created a base type "BaseType" which all the API data objects (TypeA, TypeB, ...) were inherited.

public class XmlParamHandler implements ParameterHandler<BaseType> {
    private static final Logger log = LoggerFactory.getLogger(XmlParamHandler.class);
    private static final JAXBContext jaxbContext = initContext();


    private static JAXBContext initContext() throws JAXBException {
        Class<?>[] classes = new Class[] {
            com.mycompany.BaseType.class,
            com.mycompany.TypeA.class,
            com.mycompany.TypeB.class,
            com.mycompany.TypeC.class,
            com.mycompany.TypeD.class,
            com.mycompany.TypeE.class,
            com.mycompany.TypeF.class,
        };
        JAXBContext context = JAXBContext.newInstance(classes);
        return context;
    }

    public static <T> T valueOf(String str) throws JAXBException {
        if (str == null) {
            return null;
        }
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        StringReader sr = new StringReader(str);
        @SuppressWarnings("unchecked")
        T request = (T) unmarshaller.unmarshal(sr);
        return request;
    }

    @Override
    public BaseType fromString(String s) {
        BaseType ct = null;
        try {
            return valueOf(s);
        } catch (JAXBException e) {
            return null;
        }
    }
}
like image 171
Virtually Real Avatar answered Nov 14 '22 20:11

Virtually Real