Thursday, March 5, 2015

Writing RESTful web services using Apache CXF & Spring

In an earlier blog, I showed you how to develop APIs using REST and Spring RS. Since Spring does not fully implement JAX-RS, in this blog, I will show you how to use Apache CXF with Spring to write RESTful web services. pom.xml:
<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">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.noushin</groupId>
    <artifactId>cxfws</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>cxfws</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <cxf.version>3.0.2</cxf.version>
        <junit.version>4.10</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <spring.version>4.1.1.RELEASE</spring.version>
        <spring.security.version>3.2.0.RELEASE</spring.security.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- Logging -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>

        <!-- Spring Test -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- Spring --> 
         <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- JSON Binding -->
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-jaxrs</artifactId>
            <version>1.5.7</version>
        </dependency>

        <!-- CXF -->
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-frontend-jaxrs</artifactId>
            <version>${cxf.version}</version>
        </dependency>

    </dependencies>
    
    
    <build>
        <finalName>cxfws</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
The first class you need to write is an interface to expose your web services:
package com.noushin.cxfws.user.service;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import com.noushin.cxfws.user.model.Status;
import com.noushin.cxfws.user.model.User;

@Path("/user")
public interface UserService {

    /*
     * curl  -i -v -X GET -H "Accept: application/json" http://localhost:8080/cxfws/services/user/123
     * return: {"id":"123","firstName":"John","lastName":"Smith"}
     */
    @GET
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces({ MediaType.APPLICATION_JSON })
    @Path("/{id}")
    public User getUser(@PathParam("id") String id);
    
    /*
     * curl  -i -v -X POST -H "Content-type: application/json" -H "Accept: application/json" http://localhost:8080/cxfws/services/user/ -d @param.json
     * return: {"id":"456","firstName":"Jane","lastName":"Doe"}
     */
    
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces({ MediaType.APPLICATION_JSON })
    @Path("/")
    public User saveUser(User user);
    
    /*
     * curl  -i -v -X PUT -H "Accept: application/json" http://localhost:8080/cxfws/services/user/123/abcd
     * return: {"id":"123","firstName":"abcd","lastName":"old lastname"}
     */
    @PUT
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces({ MediaType.APPLICATION_JSON })
    @Path("/{id}/{fn}")
    public User updateUser(@PathParam("id") String id, @PathParam("fn") String firstName);
    
    
    /*
     * curl  -i -v -X DELETE -H "Accept: application/json" http://localhost:8080/cxfws/services/user/123
     * return: {"message":"User removed successfully.","code":0}
     */
    @DELETE
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces({ MediaType.APPLICATION_JSON })
    @Path("/{id}")
    public Status removeUser(@PathParam("id") String id);

}
Now, you have to write the implementation class:
package com.noushin.cxfws.user.service;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.noushin.cxfws.user.dao.UserDao;
import com.noushin.cxfws.user.model.Status;
import com.noushin.cxfws.user.model.User;
import com.noushin.cxfws.user.reference.StatusCodes;

@Service("userService")
public class UserServiceImpl implements UserService {

    private final static Logger logger = Logger.getLogger(UserServiceImpl.class);
    
    @Autowired
    UserDao userDao;
    
    public User getUser(String id) {
        String msg = "getting user with id: " + id + ".";
        logger.info(msg);
        return userDao.getUser(id);
    }

    public User saveUser(User user) {
        String msg = "saving user with id: " + user.getId() + ".";
        logger.info(msg);
        return userDao.saveUser(user);
    }

    public User updateUser(String id, String firstName) {
        String msg = "updating user with id: " + id + ".";
        logger.info(msg);
        User user = new User();
        user.setId(id);
        user.setFirstName(firstName);
        return userDao.saveUser(user);
    }

    public Status removeUser(String id) {
        String msg = "removing user with id: " + id + ".";
        logger.info(msg);
        Status status = null;
        if (userDao.removeUser(id))
            return new Status(StatusCodes.FAILED, "User removed successfully.");
        return  new Status(StatusCodes.FAILED, "User could not be removed.");
    }

}

The JSON objects that will be passed to or retuned from user web services, will be presented as simple POJOS. User JSON:
package com.noushin.cxfws.user.model;

public class User {
    
    private String id;
    private String firstName;
    private String lastName;
    
    public User() {
    }
    
    public User(String id, String firstName, String lastName) {
        super();
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getId() {
        return id;
    }
    
    public void setId(String id) {
        this.id = id;
    }
    
    public String getFirstName() {
        return firstName;
    }
    
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    
    public String getLastName() {
        return lastName;
    }
    
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    
    public String toString() {
        return "User {id:" + id + ", firstName:" + firstName + ", lastName:" + lastName + "}";
    }
    
}

Status JSON:
package com.noushin.cxfws.user.model;

public class Status {
    private int code;
    private String message;
    
    
    public Status() {
    }

    public Status(int code, String message) {
        super();
        this.code = code;
        this.message = message;
    }
    
    public int getCode() {
        return code;
    }
    
    public void setCode(int code) {
        this.code = code;
    }
    
    public String getMessage() {
        return message;
    }
    
    public void setMessage(String message) {
        this.message = message;
    }
    
    
}


UserDao.java:
package com.noushin.cxfws.user.dao;

import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;

import com.noushin.cxfws.user.model.User;

@Component
public class UserDao {
    
    private final static Logger logger = Logger.getLogger(UserDao.class);

    public User getUser(String id) {
        User user = new User(id, "John", "Smith");
        String msg = "getting user with id: " + id + ".";
        logger.info(msg);
        return user;
    }
    
    public User saveUser(User user) {
        String msg = "saving user with id: " + user.getId() + ".";
        logger.info(msg);
        return user;
    }
    
    public boolean removeUser(String id) {
        String msg = "removing user with id: " + id + ".";
        logger.info(msg);
        return true;
    }
    
}


Reference data:
package com.noushin.cxf.user.reference;

public class StatusCodes {
    public final static int SUCCESS = 1;
    public final static int FAILED = 0;
    
}

~cxfws/src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">

    <display-name>Spring CXF WS Demo</display-name>
    <description>Spring CXF WS Demo</description>

    <servlet>
        <servlet-name>CXFServlet</servlet-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>

    <context-param>
        <param-name>log4jConfigLocation</param-name>
        <param-value>/WEB-INF/classes/log4j.properties</param-value>
    </context-param>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/cxfws-servlet.xml</param-value>
    </context-param>

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

</web-app>
Spring application context is in ~cxfws/src/main/webapp/WEB-INF/cxfws-servlet.xml
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jaxrs="http://cxf.apache.org/jaxrs"
    xmlns:cxf="http://cxf.apache.org/core"
    xmlns:http-conf="http://cxf.apache.org/transports/http/configuration"
    xsi:schemaLocation="
      http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
      http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd 
      http://cxf.apache.org/transports/http/configuration http://cxf.apache.org/schemas/configuration/http-conf.xsd">

    <context:component-scan base-package="com.noushin.cxfws.user" />
    <context:annotation-config />

    <jaxrs:server id="userServer" address="/">
        <jaxrs:serviceBeans>
            <ref bean="userService" />
        </jaxrs:serviceBeans>
        <jaxrs:providers>
            <bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider" />
        </jaxrs:providers>
        <jaxrs:extensionMappings>
            <entry key="json" value="application/json" />
        </jaxrs:extensionMappings>
        <jaxrs:features>
            <cxf:logging/>
        </jaxrs:features>
    </jaxrs:server>

    <bean id="userServiceImpl" class="com.noushin.cxfws.user.service.UserServiceImpl" />

</beans>