December 15th, 2013

Polymorphic Payloads in RESTful API using Apache CXF/JAX-RS

Create a semantic, RESTful JSONAPI formatted API using Apache CXF and JAX-RS

— Matthew Fellows —

So, I’m lazy, and I really wanted a way that I could just throw an annotation on a class, and maybe some spring configuration, and have an RESTful API exposed for any object properly configured with javax.persistence annotations. For this implementation, I was using the Open Source FUSE ESB v4.4.0-fuse-00-43  (Apache ServiceMix) and also Spring Data for JPA repositories (EclipseLink).

It turns out that polymorphic payloads in CXF is actually more difficult than you might think. In particular due to the fact that I wanted to standardised in/out messages and the way that wiring happens in bean configurations CXF could never quite work out what object was being (un)marshalled. What follows is a rough overview of what I’ve done (written in code) and really just highlights the painful intricacies and verbosity of Java and its ecosystem to achieve something that I could probably do in a handful of lines in Ruby or Scala.

The objectives

  • Consistent API: Support standard GET, POST, PUT & DELETE semantics, pagination, sorting and JSONAPI (ID) formatted responses
  • Centralised security and auditing layer (OAuth-based)
  • Ability to override on a per-Class basis
  • Avoid duplication!

API Interface

Given an API interface like the following, it is actually possible.

import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
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.QueryParam;

import au.com.substantiate.service.rest.common.Constants;
import au.com.substantiate.service.rest.common.ResponseVO; // Standard i/o representation
import au.com.substantiate.service.rest.common.service.annotation.CRUD; // Literally, just an annotation to place on service objects
import au.com.substantiate.service.rest.common.service.exception.CrudServiceException;

/**
 * CrudService is a generic interface used to access a database layer via RESTful services.
 *
 * See {@link RestfulCrudService} for the default implementation.
 *
 */
public interface CrudService {

 @Path("/")
 @GET
 public ResponseVO list(
 @QueryParam("page") @DefaultValue(Constants.DEFAULT_PAGE) int page,
 @QueryParam("page.size") @DefaultValue(Constants.DEFAULT_PAGE_SIZE) int size,
 @QueryParam("page.sort") @DefaultValue(Constants.DEFAULT_PAGE_SORT) String sortProperty,
 @QueryParam("page.sort.direction") @DefaultValue(Constants.DEFAULT_PAGE_SORT_DIRECTION) String sortDirection) throws CrudServiceException;

 /**
 * Delete an Entity given its id.
 *
 * @param id The database ID of the entity to delete.
 * @throws CrudServiceException
 */
 @Path("/{id}")
 @DELETE
 @CRUD
 public void delete(@PathParam("id") long id) throws CrudServiceException;

 /**
 * Create a new entity in the database.
 *
 * @param object The entity to persist.
 * @return The newly created entity.
 * @throws CrudServiceException
 */
 @Path("/")
 @POST
 @CRUD
 public T create(T object) throws CrudServiceException;

 /**
 * Get an entity from the database by its id.
 *
 * @param id The id of the entity to retrieve.
 * @return The entity
 * @throws CrudServiceException
 */
 @Path("/{id}")
 @GET
 @CRUD
 public T get(@PathParam("id") long id) throws CrudServiceException;

/**
 * Update an existing entity in the database.
 *
 * @param object The entity to update.
 * @return
 * @throws CrudServiceException
 */
 @Path("/")
 @PUT
 @CRUD
 public T update(T object) throws CrudServiceException;
}

Response Payload

Here is the generic Response object(s):

/**
 * Not Copyright Substantiate 2012.
 */
package au.com.substantiate.service.rest.common;

import java.util.List;

import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

/**
 * @author mfellows
 *
 */
@XmlRootElement
public class ResponseVO {

protected PageVO page;

 protected ErrorMessageVO error;

@XmlElement
 protected ContentHolder contents;

 public ResponseVO() {
 this.contents = null;
 this.error = null;
 this.page = null;
 }

 public ResponseVO(PageVO page, List contents, ErrorMessageVO error) {
 this.page = page;
 this.error = error;

 this.setContents(contents);
 }

 public ErrorMessageVO getError() {
 return error;
 }

public void setError(ErrorMessageVO error) {
 this.error = error;
 }

 public List getContents() {
 if (contents != null) {
 return contents.getContents();
 }
 return null;
 }

public void setContents(ContentHolder ch) {
 this.contents = ch;
 }

 protected void setContents(List contents) {
 this.contents = new ContentHolder();
 this.contents.setContents(contents);
 }

public PageVO getPage() {
 return page;
 }

public void setPage(PageVO page) {
 this.page = page;
 }

 public static class ContentHolder {
 @XmlAnyElement(lax=true)
 private List item;

 public ContentHolder() {
 this.item = null;
 }

 public void setContents(List contents) {
 this.item = contents;
 }

 @XmlTransient
 public List getContents() {
 return item;
 }
 }
}
/**
 * Copyright Substantiate 2012.
 */
package au.com.substantiate.service.rest.common;

import javax.xml.bind.annotation.XmlRootElement;

import org.springframework.data.domain.Page;

/**
 * @author mfellows
 * @param <T>
 *
 */
@XmlRootElement
public class PageVO {
 protected int pageNumber;

protected int pageSize;

protected Sort sort;

 protected int totalPages;

 protected boolean isFirstPage;

 protected boolean isLastPage;

 protected boolean hasNextPage;

 protected boolean hasPreviousPage;

 protected long totalElements;

 protected int numberOfElements;

 public PageVO() {
 this.setDefaults();
 }

 public PageVO(Page<?> page) {
 if (page != null) {
 this.pageNumber = page.getNumber();
 this.pageSize = page.getSize();
 this.numberOfElements = page.getNumberOfElements();
 this.totalElements = page.getTotalElements();
 this.totalPages = page.getTotalPages();
 this.hasPreviousPage = page.hasPreviousPage();
 this.hasNextPage = page.hasNextPage();
 this.isFirstPage = page.isFirstPage();
 this.isLastPage = page.isLastPage();

 if (page.getSort() != null) {
 this.sort = new Sort(page.getSort());
 } else {
 this.sort = null;
 }
 } else {
 this.setDefaults();
 }
 }

 private void setDefaults() {
 this.pageNumber = 0;
 this.pageSize = 0;
 this.sort = null;
 }

 /* (non-Javadoc)
 * @see org.springframework.data.domain.Page#getNumber()
 */
 public int getNumber() {
 return pageNumber;
 }

/* (non-Javadoc)
 * @see org.springframework.data.domain.Page#getSize()
 */
 public int getSize() {
 return pageSize;
 }

/* (non-Javadoc)
 * @see org.springframework.data.domain.Page#getTotalPages()
 */
 public int getTotalPages() {
 return totalPages;
 }

/* (non-Javadoc)
 * @see org.springframework.data.domain.Page#getNumberOfElements()
 */
 public int getNumberOfElements() {
 return this.numberOfElements;
 }

/* (non-Javadoc)
 * @see org.springframework.data.domain.Page#getTotalElements()
 */
 public long getTotalElements() {
 return this.totalElements;
 }

/* (non-Javadoc)
 * @see org.springframework.data.domain.Page#hasPreviousPage()
 */
 public boolean hasPreviousPage() {
 return this.hasPreviousPage;
 }

/* (non-Javadoc)
 * @see org.springframework.data.domain.Page#isFirstPage()
 */
 public boolean isFirstPage() {
 return this.isFirstPage;
 }

/* (non-Javadoc)
 * @see org.springframework.data.domain.Page#hasNextPage()
 */
 public boolean hasNextPage() {
 return this.hasNextPage;
 }

/* (non-Javadoc)
 * @see org.springframework.data.domain.Page#isLastPage()
 */
 public boolean isLastPage() {
 return this.isLastPage;
 }

/* (non-Javadoc)
 * @see org.springframework.data.domain.Page#getSort()
 */
 public Sort getSort() {
 return sort;
 }

public int getPageNumber() {
 return pageNumber;
 }

public void setPageNumber(int pageNumber) {
 this.pageNumber = pageNumber;
 }

public int getPageSize() {
 return pageSize;
 }

public void setPageSize(int pageSize) {
 this.pageSize = pageSize;
 }

public void setSort(Sort sort) {
 this.sort = sort;
 }

public void setTotalPages(int totalPages) {
 this.totalPages = totalPages;
 }

public boolean isHasNextPage() {
 return hasNextPage;
 }

public void setHasNextPage(boolean hasNextPage) {
 this.hasNextPage = hasNextPage;
 }

public boolean isHasPreviousPage() {
 return hasPreviousPage;
 }

public void setHasPreviousPage(boolean hasPreviousPage) {
 this.hasPreviousPage = hasPreviousPage;
 }

public void setFirstPage(boolean isFirstPage) {
 this.isFirstPage = isFirstPage;
 }

public void setLastPage(boolean isLastPage) {
 this.isLastPage = isLastPage;
 }

public void setTotalElements(long totalElements) {
 this.totalElements = totalElements;
 }

public void setNumberOfElements(int numberOfElements) {
 this.numberOfElements = numberOfElements;
 }
}

/**
 * Copyright Substantiate 2012.
 */
package au.com.substantiate.service.rest.common;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import org.springframework.util.StringUtils;

/**
 * @author mfellows
 * TODO: create fluent API - from(org.spring...Sort) and to(org.spring..Sort)
 */
@XmlRootElement
public class Sort implements Iterable<Sort.Order>, Serializable {

private static final long serialVersionUID = 5737186511678863905L;
 public static final Direction DEFAULT_DIRECTION = Direction.ASC;

@XmlElement
 private List<Order> orderVOs;

public void setOrders(Order...orders) {
 this.orderVOs = Arrays.asList(orders);
 }

 public void setOrderVOs(List<Order> orderVOs) {
 this.orderVOs = orderVOs;
 }

 public Sort() {
 orderVOs = new LinkedList<Order>();
 }

 public Sort(org.springframework.data.domain.Sort sort) {
 if (sort != null) {
 orderVOs = new LinkedList<Order>();
 for(org.springframework.data.domain.Sort.Order o : sort) {
 orderVOs.add(new Order(o));
 }
 } else {
 orderVOs = new LinkedList<Order>();
 }
 }

 public Sort(Order... orders) {
 this(Arrays.asList(orders));
 }

/**
 * Creates a new {@link Sort} instance.
 *
 * @param orderVOs
 * must not be {@literal null} or contain {@literal null} or
 * empty strings
 */
 public Sort(List<Order> orderVOs) {

if (null == orderVOs || orderVOs.isEmpty()) {
 throw new IllegalArgumentException(
 "You have to provide at least one sort property to sort by!");
 }

this.orderVOs = orderVOs;
 }

/**
 * Creates a new {@link Sort} instance. Order defaults to
 * {@value Direction#ASC}.
 *
 * @param properties
 * must not be {@literal null} or contain {@literal null} or
 * empty strings
 */
 public Sort(String... properties) {

this(DEFAULT_DIRECTION, properties);
 }

/**
 * Creates a new {@link Sort} instance.
 *
 * @param direction
 * defaults to {@value Sort#DEFAULT_DIRECTION} (for
 * {@literal null} cases, too)
 * @param properties
 * must not be {@literal null} or contain {@literal null} or
 * empty strings
 */
 public Sort(Direction direction, String... properties) {

this(direction, properties == null ? new ArrayList<String>() : Arrays
 .asList(properties));
 }

/**
 * Creates a new {@link Sort} instance.
 *
 * @param direction
 * @param properties
 */
 public Sort(Direction direction, List<String> properties) {

if (properties == null || properties.isEmpty()) {
 throw new IllegalArgumentException(
 "You have to provide at least one property to sort by!");
 }

this.orderVOs = new ArrayList<Order>(properties.size());

for (String property : properties) {
 this.orderVOs.add(new Order(direction, property));
 }
 }

 /**
 * Returns a Spring Data compliant {@link Sort} object
 *
 */
 public org.springframework.data.domain.Sort asSpringDataSort() {
 List<org.springframework.data.domain.Sort.Order> orders = new LinkedList<org.springframework.data.domain.Sort.Order>();

 for (Order o: this.orderVOs) {
 orders.add(o.asSpringDataOrder());
 }
 org.springframework.data.domain.Sort sort = new org.springframework.data.domain.Sort(orders);
 return sort;
 }

/**
 * Returns the order registered for the given property.
 *
 * @param property
 * @return
 */
 public Order getOrderVOFor(String property) {

for (Order orderVO : this) {
 if (orderVO.getProperty().equals(property)) {
 return orderVO;
 }
 }

return null;
 }

/*
 * (non-Javadoc)
 *
 * @see java.lang.Iterable#iterator()
 */
 public Iterator<Order> iterator() {

return this.orderVOs.iterator();
 }

/*
 * (non-Javadoc)
 *
 * @see java.lang.Object#equals(java.lang.Object)
 */
 @Override
 public boolean equals(Object obj) {

if (this == obj) {
 return true;
 }

if (!(obj instanceof Sort)) {
 return false;
 }

Sort that = (Sort) obj;

return this.orderVOs.equals(that.orderVOs);
 }

/*
 * (non-Javadoc)
 *
 * @see java.lang.Object#hashCode()
 */
 @Override
 public int hashCode() {

int result = 17;
 result = 31 * result + orderVOs.hashCode();
 return result;
 }

/*
 * (non-Javadoc)
 *
 * @see java.lang.Object#toString()
 */
 @Override
 public String toString() {

return StringUtils.collectionToCommaDelimitedString(orderVOs);
 }

/**
 * Enumeration for sort directions.
 *
 * @author Oliver Gierke
 */
 public static enum Direction {

ASC, DESC;

/**
 * Returns the {@link Direction} enum for the given {@link String}
 * value.
 *
 * @param value
 * @return
 */
 public static Direction fromString(String value) {

try {
 return Direction.valueOf(value.toUpperCase(Locale.US));
 } catch (Exception e) {
 throw new IllegalArgumentException(
 String.format(
 "Invalid value '%s' for orders given! Has to be either 'desc' or 'asc' (case insensitive).",
 value), e);
 }
 }
 }

/**
 * Property implements the pairing of an {@code Order} and a property. It is
 * used to provide input for {@link Sort}
 *
 * @author Oliver Gierke
 */
 @XmlRootElement
 public static class Order {

@XmlElement
 private Direction direction;
 @XmlElement
 private String property;

 /**
 * Creates a new {@link Order} instance. if order is {@literal null}
 * then order defaults to {@value Sort#DEFAULT_DIRECTION}
 *
 * @param direction
 * can be {@code null}
 * @param property
 * must not be {@code null} or empty
 */
 public Order(Direction direction, String property) {

if (property == null || "".equals(property.trim())) {
 throw new IllegalArgumentException(
 "Property must not null or empty!");
 }

this.direction = direction == null ? DEFAULT_DIRECTION : direction;
 this.property = property;
 }

 public Order() {

 }

 public Order(org.springframework.data.domain.Sort.Order order) {
 this.direction = (order.getDirection() == org.springframework.data.domain.Sort.Direction.ASC) ? Direction.ASC : Direction.DESC;
 this.property = order.getProperty();
 }

 public org.springframework.data.domain.Sort.Order asSpringDataOrder() {
 org.springframework.data.domain.Sort.Direction d = (this.direction == Direction.ASC) ? org.springframework.data.domain.Sort.Direction.ASC : org.springframework.data.domain.Sort.Direction.DESC;
 return new org.springframework.data.domain.Sort.Order(d, this.property);
 }

/**
 * Creates a new {@link Order} instance. Takes a single property. Order
 * defaults to {@value SortVO.DEFAULT_ORDER}
 *
 * @param property
 * - must not be {@code null} or empty
 */
 public Order(String property) {

this(DEFAULT_DIRECTION, property);
 }

public static List<Order> create(Direction direction,
 Iterable<String> properties) {

List<Order> orderVOs = new ArrayList<Sort.Order>();
 for (String property : properties) {
 orderVOs.add(new Order(direction, property));
 }
 return orderVOs;
 }

/**
 * Returns the order the property shall be sorted for.
 *
 * @return
 */
 public Direction getDirection() {

return direction;
 }

/**
 * Returns the property to order for.
 *
 * @return
 */
 public String getProperty() {

return property;
 }

/**
 * Returns whether sorting for this property shall be ascending.
 *
 * @return
 */
 public boolean isAscending() {

return this.direction.equals(Direction.ASC);
 }

/**
 * Returns a new {@link Order} with the given {@link Order}.
 *
 * @param order
 * @return
 */
 public Order with(Direction order) {

return new Order(order, this.property);
 }

/**
 * Returns a new {@link Sort} instance for the given properties.
 *
 * @param properties
 * @return
 */
 public Sort withProperties(String... properties) {

return new Sort(this.direction, properties);
 }

/*
 * (non-Javadoc)
 *
 * @see java.lang.Object#hashCode()
 */
 @Override
 public int hashCode() {

int result = 17;

result = 31 * result + direction.hashCode();
 result = 31 * result + property.hashCode();

return result;
 }

/*
 * (non-Javadoc)
 *
 * @see java.lang.Object#equals(java.lang.Object)
 */
 @Override
 public boolean equals(Object obj) {

if (this == obj) {
 return true;
 }

if (!(obj instanceof Order)) {
 return false;
 }

Order that = (Order) obj;

return this.direction.equals(that.direction)
 && this.property.equals(that.property);
 }

/*
 * (non-Javadoc)
 *
 * @see java.lang.Object#toString()
 */
 @Override
 public String toString() {

return String.format("%s: %s", property, direction);
 }
 }
}

Implementation

Example implementation Class. Please do NOT copy/paste this snippet, it needs error handling and of course a security layer – an exercise left for the reader.

package au.com.substantiate.service.rest.common.service;

import java.util.List;

import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
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.QueryParam;
import javax.ws.rs.core.Context;

import org.apache.cxf.jaxrs.ext.MessageContext;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.repository.PagingAndSortingRepository;

import au.com.substantiate.service.rest.common.Constants;
import au.com.substantiate.service.rest.common.ErrorMessageVO;
import au.com.substantiate.service.rest.common.PageVO;
import au.com.substantiate.service.rest.common.PaginationRequest;
import au.com.substantiate.service.rest.common.ResponseVO;
import au.com.substantiate.service.rest.common.Sort;
import au.com.substantiate.service.rest.common.service.annotation.CRUD;
import au.com.substantiate.service.rest.common.service.exception.CrudServiceException;
import au.com.substantiate.service.rest.common.service.exception.EntityNotFoundException;

/**
 *
 * @author mfellows
 *
 * @param <R> The concrete Repository type (eg. GPSEventRepository) to use in database searches.
 * @param <T> The concrete Entity type (eg. GPSEvent) to return as the object.
 */
public abstract class RestfulCrudService<R extends PagingAndSortingRepository<T, Long>, T> implements CrudService<T> {

private static Logger logger = org.slf4j.LoggerFactory.getLogger(RestfulCrudService.class);

 @Autowired
 R repository;

@Context
 public MessageContext mc;

 @Override
 @Path("/")
 @GET
 public ResponseVO<T> list(
 @QueryParam("page") @DefaultValue(Constants.DEFAULT_PAGE) int page,
 @QueryParam("page.size") @DefaultValue(Constants.DEFAULT_PAGE_SIZE) int size,
 @QueryParam("page.sort") @DefaultValue(Constants.DEFAULT_PAGE_SORT) String sortProperty,
 @QueryParam("page.sort.direction") @DefaultValue(Constants.DEFAULT_PAGE_SORT_DIRECTION) String sortDirection) throws CrudServiceException {

 logger.debug("Request with details: page=" + page + ",page.size=" + size + ",page.sort=" + sortProperty + ",page.sort.direction=" + sortDirection);

Page<T> pageResult = this.repository.findAll(createPageRequest(page, size, sortProperty, sortDirection));

 return this.generateResponse(pageResult);
 }

/* (non-Javadoc)
 * @see au.com.substantiate.service.rest.common.service.CrudService#delete(java.lang.Object)
 */
 @Path("/{id}")
 @DELETE
 @CRUD
 public void delete(@PathParam("id") long id) throws CrudServiceException {
 try {
 this.repository.delete(id);
 } catch (DataAccessException e) {
 throw new CrudServiceException(new ErrorMessageVO(ErrorMessageVO.ErrorCode.ERROR_ENTITY_NOT_FOUND, "Object could not be deleted as it was not found. Nested Exception: " + e.getLocalizedMessage()));
 } catch (Exception e) {

 }
 }

/* (non-Javadoc)
 * @see au.com.substantiate.service.rest.common.service.CrudService#create(java.lang.Object)
 */
 @Path("/")
 @POST
 @CRUD
 public T create(T object) throws CrudServiceException {
 try {
 T obj = this.repository.save(object);
 if (obj == null) {
 throw new CrudServiceException(new ErrorMessageVO(ErrorMessageVO.ErrorCode.ERROR_ENTITY_NOT_CREATED, "Object could not be created."));
 }
 return obj;
 } catch (DataAccessException e) {
 throw new CrudServiceException(new ErrorMessageVO(ErrorMessageVO.ErrorCode.ERROR_ENTITY_NOT_SAVED, "Object could not be saved. Nested Exception: " + e.getLocalizedMessage()));
 } catch (Exception e) {
 if (e.getCause() != null && e.getCause().getCause() != null && e.getCause().getCause().getCause() != null && e.getCause().getCause().getCause() instanceof java.sql.SQLIntegrityConstraintViolationException) {
 throw new CrudServiceException(new ErrorMessageVO(ErrorMessageVO.ErrorCode.ERROR_ENTITY_NOT_CREATED_ENTITY_EXISTS, "Object could not be saved as it already exists. Nested Exception: " + e.getLocalizedMessage()));
 } else {
 throw new CrudServiceException(new ErrorMessageVO(ErrorMessageVO.ErrorCode.ERROR_ENTITY_NOT_SAVED, "Object could not be saved. Nested Exception: " + e.getLocalizedMessage()));
 }
 }
 }

/* (non-Javadoc)
 * @see au.com.substantiate.service.rest.common.service.CrudService#get(java.lang.Long)
 */
 @Path("/{id}")
 @GET
 @CRUD
 public T get(@PathParam("id") long id) throws CrudServiceException {
 logger.debug("Finding object with id: " + id);
 T obj = this.repository.findOne(id);

 if (obj == null) {
 logger.debug("No object found. Throwing Exception");
 throw new CrudServiceException(new ErrorMessageVO(ErrorMessageVO.ErrorCode.ERROR_ENTITY_NOT_FOUND, "Object not found"));
 }

 logger.debug("Returning object: " + obj.toString());

 return obj;
 }

/* (non-Javadoc)
 * @see au.com.substantiate.service.rest.common.service.CrudService#update(java.lang.Object)
 */
 @Path("/")
 @PUT
 @CRUD
 public T update(T object) throws CrudServiceException {
 try {
 T updated = this.repository.save(object);
 if (updated == null) {
 throw new EntityNotFoundException("Could not save object as original entity was not found.");
 }
 return updated;
 } catch (DataAccessException e) {
 throw new CrudServiceException(new ErrorMessageVO(ErrorMessageVO.ErrorCode.ERROR_ENTITY_NOT_SAVED, "Could not save object. Nested Exception: " + e.getLocalizedMessage()));
 } catch (Exception e) {
 throw new CrudServiceException(new ErrorMessageVO(ErrorMessageVO.ErrorCode.ERROR_ENTITY_NOT_SAVED, "Could not save object. Nested Exception: " + e.getLocalizedMessage()));
 }
 }

/**
 * Generate a ResponseVO object for the request.
 *
 * @param page
 * @param error
 * @return
 */
 protected ResponseVO<T> generateResponse(Page<T> page, ErrorMessageVO error) {
 PageVO pageVo = null;
 List<T> contents = null;
 if (page != null) {
 pageVo = new PageVO(page);
 contents = page.getContent();
 }

 logger.debug("Retrieved page object from repository, with contents of size: " + ((contents != null) ? contents.size() : 0));
 logger.debug("Converted contents to VOs, with contents of size: " + ((contents != null) ? contents.size() : 0));
 ResponseVO<T> response = new ResponseVO<T>(pageVo, contents, null);

 return response;
 }
 protected ResponseVO<T> generateResponse(Page<T> page) {
 return generateResponse(page, new ErrorMessageVO());
 }

 protected PageRequest createPageRequest(int page, int size, String sortProperty, String sortDirection) {
 PageRequest request;
 if (sortProperty.isEmpty()) {
 logger.debug("Creating Page Request with NO sort details: page=" + page + ",page.size=" + size + ",page.sort=" + sortProperty + ",page.sort.direction=" + sortDirection);
 request = new PageRequest(page, size);
 } else {
 logger.debug("Creating Page Request WITH sort details: page=" + page + ",page.size=" + size + ",page.sort=" + sortProperty + ",page.sort.direction=" + sortDirection);
 org.springframework.data.domain.Sort.Direction dir = (sortDirection.equalsIgnoreCase("DESC")) ? org.springframework.data.domain.Sort.Direction.DESC : org.springframework.data.domain.Sort.Direction.ASC;
 org.springframework.data.domain.Sort sort = new org.springframework.data.domain.Sort(new org.springframework.data.domain.Sort.Order(dir, sortProperty));
 request = new PageRequest(page, size, sort);
 }
 return request;
 }

 protected PaginationRequest createPaginationRequest(int page, int size, String sortProperty, String sortDirection) {
 PaginationRequest request;
 if (sortProperty.isEmpty()) {
 logger.debug("Creating Pagination Request with NO sort details: page=" + page + ",page.size=" + size + ",page.sort=" + sortProperty + ",page.sort.direction=" + sortDirection);
 request = new PaginationRequest(page, size);
 } else {
 logger.debug("Creating Pagination Request WITH sort details: page=" + page + ",page.size=" + size + ",page.sort=" + sortProperty + ",page.sort.direction=" + sortDirection);
 Sort.Direction dir = (sortDirection.equalsIgnoreCase("DESC")) ? Sort.Direction.DESC : Sort.Direction.ASC;
 Sort sort = new Sort(new Sort.Order(dir, sortProperty));
 request = new PaginationRequest(page, size, sort);
 }
 return request;
 }

public R getRepository() {
 return repository;
 }

public void setRepository(R repository) {
 this.repository = repository;
 }
}

JAX-RS Client Configuration

When configuring the client, a Message Body Reader / Writer class is required to allow CXF to determine what class to (un)marshall to:

/**
 * Copyright Substantiate 2012.
 */
package au.com.substantiate.service.rest.common.service.reader;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.List;

import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;

import org.codehaus.jettison.json.JSONObject;
import org.codehaus.jettison.mapped.Configuration;
import org.codehaus.jettison.mapped.MappedNamespaceConvention;
import org.codehaus.jettison.mapped.MappedXMLStreamReader;
import org.codehaus.jettison.mapped.MappedXMLStreamWriter;
import org.slf4j.Logger;

import au.com.substantiate.service.rest.common.service.CrudService;
import au.com.substantiate.service.rest.common.service.annotation.CRUD;

/**
 * Generic Object Unmarshaller for use by the {@link CrudService} class. To use correctly with generic types,
 * JAXb must be notified of the generic type to be used in advance, for example GPSEvent for use in location-service-impl.
 *
 */
@Provider
@Consumes({ "application/xml", "application/*+xml", "text/xml",
 "application/json" })
@Produces({ "application/xml", "application/*+xml", "text/xml",
"application/json" })
public class CrudServiceObjectBodyReader<T> implements MessageBodyReader<Object>, MessageBodyWriter<Object> {

private static Logger logger = org.slf4j.LoggerFactory.getLogger(CrudServiceObjectBodyReader.class);

 private Class<T> clazz;

 // Packages to include in the marshall/unmarshall process. Better than clazz obviously.
 private String packages;

 private static final Charset UTF8 = Charset.forName("utf-8");

 /*
 * (non-Javadoc)
 *
 * @see javax.ws.rs.ext.MessageBodyReader#isReadable(java.lang.Class,
 * java.lang.reflect.Type, java.lang.annotation.Annotation[],
 * javax.ws.rs.core.MediaType)
 */
 @Override
 public boolean isReadable(Class<?> type, Type genericType,
 Annotation[] annotations, MediaType mediaType) {

 logger.debug("CrudServiceObjectReader/Writer - isReadable");

 for (Annotation a: annotations) {
 if (a.annotationType().equals(CRUD.class)) {
 logger.debug("CrudServiceObjectReader/Writer - isReadable - CRUD annotation found!");
 return true;
 }
 }
 return false;
 }

 private boolean isJsonWriter(MultivaluedMap<String, Object> httpHeaders) {
 logger.debug("CrudServiceObjectReader/Writer - isJsonWriter");

 // JSON
 if (httpHeaders.containsKey("Accept")) {
 List<Object> contentTypeList = httpHeaders.get("Accept");
 for (Object ct: contentTypeList) {
 if (ct.toString().equalsIgnoreCase("application/json")) {
 return true;
 }
 }
 }

return false;
 }
 private boolean isJsonReader(MultivaluedMap<String, String> httpHeaders) {
 logger.debug("CrudServiceObjectReader/Writer - isJsonReader");
 // JSON
 if (httpHeaders.containsKey("Content-Type")) {
 List<String> contentTypeList = httpHeaders.get("Content-Type");
 for (String ct: contentTypeList) {
 if (ct.equalsIgnoreCase("application/json")) {
 return true;
 }
 }
 }

 return false;
 }

/*
 * (non-Javadoc)
 *
 * @see javax.ws.rs.ext.MessageBodyReader#readFrom(java.lang.Class,
 * java.lang.reflect.Type, java.lang.annotation.Annotation[],
 * javax.ws.rs.core.MediaType, javax.ws.rs.core.MultivaluedMap,
 * java.io.InputStream)
 */
 @Override
 public Object readFrom(Class<Object> object, Type genericType,
 Annotation[] annotations, MediaType mediaType,
 MultivaluedMap<String, String> httpHeaders, InputStream is)
 throws IOException, WebApplicationException {

logger.debug("CrudServiceObjectReader/Writer - readFrom");

 // JSON
 if (isJsonReader(httpHeaders)) {
 return this.handleJson(is);
 }

 // XML
 JAXBContext jc;
 try {
 jc = getJAXBContext();
 Unmarshaller um = jc.createUnmarshaller();
 T obj = cast(um.unmarshal(is));
 return obj;
 } catch (JAXBException e) {
 e.printStackTrace();
 throw new IOException("Cannot convert from Object to " + clazz.getName() + ". Nested Exception is " + e.getLocalizedMessage());
 }
 }

protected Object handleJson(InputStream is) throws IOException {
 logger.debug("CrudServiceObjectReader/Writer - handleJson");
 JAXBContext jc;
 try {
 jc = getJAXBContext();
 Unmarshaller um = jc.createUnmarshaller();
 Configuration config = new Configuration();
 MappedNamespaceConvention con = new MappedNamespaceConvention(config);
 JSONObject json = new JSONObject(is.toString());
 XMLStreamReader xmlStreamReader = new MappedXMLStreamReader(json, con);

return um.unmarshal(xmlStreamReader, clazz);
 } catch (Exception e) {
 throw new IOException("Cannot convert from Object to " + clazz.getName() + ". Nested Exception is " + e.getLocalizedMessage());
 }
 }

private JAXBContext getJAXBContext() throws JAXBException {
 logger.debug("CrudServiceObjectReader/Writer - getJAXBContext");
 // Would like to use packages here and make configuration simpler, although it obviously
 if (this.packages != null) {
 return JAXBContext.newInstance("au.com.substantiate.api, au.com.substantiate.api.model");
 } else if (this.clazz != null) {
 return JAXBContext.newInstance(clazz);
 }
 return null;
 }

 @SuppressWarnings("unchecked")
 protected static <T> T cast(Object o) {
 return (T) o;
 }

public Class<T> getClazz() {
 return clazz;
 }

public void setClazz(Class<T> clazz) {
 this.clazz = clazz;
 }

/* (non-Javadoc)
 * @see javax.ws.rs.ext.MessageBodyWriter#isWriteable(java.lang.Class, java.lang.reflect.Type, java.lang.annotation.Annotation[], javax.ws.rs.core.MediaType)
 */
 @Override
 public boolean isWriteable(Class<?> type, Type genericType,
 Annotation[] annotations, MediaType mediaType) {
 logger.debug("CrudServiceObjectReader/Writer - isWritable");
 return this.isReadable(type, genericType, annotations, mediaType);
 }

/* (non-Javadoc)
 * @see javax.ws.rs.ext.MessageBodyWriter#getSize(java.lang.Object, java.lang.Class, java.lang.reflect.Type, java.lang.annotation.Annotation[], javax.ws.rs.core.MediaType)
 */
 @Override
 public long getSize(Object t, Class<?> type, Type genericType,
 Annotation[] annotations, MediaType mediaType) {
 return -1;
 }

/* (non-Javadoc)
 * @see javax.ws.rs.ext.MessageBodyWriter#writeTo(java.lang.Object, java.lang.Class, java.lang.reflect.Type, java.lang.annotation.Annotation[], javax.ws.rs.core.MediaType, javax.ws.rs.core.MultivaluedMap, java.io.OutputStream)
 */
 @Override
 public void writeTo(Object t, Class<?> type, Type genericType,
 Annotation[] annotations, MediaType mediaType,
 MultivaluedMap<String, Object> httpHeaders,
 OutputStream entityStream) throws IOException,
 WebApplicationException {
 logger.debug("CrudServiceObjectReader/Writer - writeTo");

JAXBContext jc;
 try {
 jc = getJAXBContext();
 Marshaller marshaller = jc.createMarshaller();

// JSON
 if (isJsonWriter(httpHeaders)) {
 Configuration config = new Configuration();
 MappedNamespaceConvention con = new MappedNamespaceConvention(config);
 XMLStreamWriter xmlStreamWriter = new MappedXMLStreamWriter(con, new OutputStreamWriter(entityStream, UTF8));
 marshaller.marshal(t, xmlStreamWriter);
 } else {
 // XML
 marshaller.marshal(t, entityStream);
 }
 } catch (Exception e) {
 throw new IOException("Cannot convert from Object to " + clazz.getName() + ". Nested Exception is " + e.getLocalizedMessage());
 }

 }

public String getPackages() {
 return packages;
 }

public void setPackages(String packages) {
 this.packages = packages;
 }
}

When the client is created, you need to add the CrudServiceObjectBodyReader to the list of providers to the client:

<jaxrs:client id="smsRestClient"
 address="http://${host.name}:${port}/cxf/sms-service" serviceClass="au.com.substantiate.integration.foo.FooService"
 inheritHeaders="true">
 <jaxrs:headers>
 <entry key="Accept" value="${header.accept}" />
 </jaxrs:headers>
 <jaxrs:features>
 <cxf:logging />
 </jaxrs:features>
 <jaxrs:providers>
 <ref bean="jaxbProvider" />
 <ref bean="jsonJaxbProvider" />
 <bean
 class="au.com.substantiate.service.rest.common.service.reader.CrudServiceObjectBodyReader">
 <property name="clazz">
 <value type="java.lang.Class">au.com.substantiate.api.foo.Foo"
 </value>
 </property>
 </bean>
 </jaxrs:providers>
 </jaxrs:client>

And the finale, an example API Service and Implementation (deployed as separate bundles in OSGi of course). Assuming the ‘User’ Object has been created and annotated with proper JPA annotations, this will create a RESTful API on that Object:

@Path("/users")
public interface UserService extends CrudService<User> {

}

And the Implementation Class

@Path("/users")
@Produces({"application/xml","application/json", "text/xml"})
public class UserServiceImpl extends RestfulCrudService&lt;UserRepository, User&gt; implements UserService {

private static Logger logger = org.slf4j.LoggerFactory.getLogger(UserServiceImpl.class);

@Autowired
UserRepository repository; // defined as an OSGi service or at the very least a spring configuration

}