Lightweight REST JEE Arch Pattern

Motivation

Creating modern software always requires a number of things to do. One of these things is to persist data and to access persisted data. Within the Java Enterprise ecosystem this basic task evolved up to a point where using objects in Java as representations of data in a database, has become an easy to use commodity. With RESTful web services (added to JEE in version 5) attached to these entities, even network access to persisted data has become very easy. 
Yet still a lot of boiler plate code needs to be written and creating JEE application quickly remains a challenge. Therefore approach described in this article shall provide a guide in how to setup an architecture implementing persistence and a RESTful interface layer. To do that properly a (reusable) pattern of inheritance + generic data types is proposed. This (yet unnamed) pattern is applied in persistence, Enterprise Java Beans and in RESTful web service implementation and therefore proved to be quite flexible.
A fully working example application of this pattern can be found on Sourceforge. Besides using described pattern it also contains an Javascript based client application that serves a UI to the Java based back end. This rigorous separation of UI is also intended as it helps to demonstrate architecture approach without mixing with UI issues.

Sourcecode

Lightweight REST JEE Arch Pattern

Purpose

You have a group of objects which are different in its structure but should implement the same basic behavior.

Scenario

You have a lots of data objects in your application for whom the standard CRUD-functionality should be provided.

Description

E.g. you are using JPA for data access and you have create several entities. It should be possible to do simple CRUD-operations with all the objects. Using an AbstractDao is a common way to handle the implementation of the basic stuff. An AbstractEntity is created as base class for all entities. In the AbstractDao general CRUD functions are implemented using the AbstractEntity. E.g. http://sidaof.sourceforge.net/sidaof/ is a project, which implements an AbstractDao-class binding the concrete entity to the AbstractDao.

The architecture presented here goes further and uses this pattern through all layers of the entire architecture: business services (EJBs), restful webservices, junit-tests. Data objects extends the class "AbstractEntity". Each kind of service (EJB, restful webservice, jUnit-testclass) implements the general functionality around the data objects in an abstract class. For each data object and each kind of service contreted service-implementations are added. To prevent class-casts, Generics ensure the usage of concrete data object in the abstract part of the implementation.

The following class-diagram provides an overview of the architecture. The description afterwards is given a better understanding of the diagramm.

Advantage

You are really fast on creating or extending the backend of small JEE-application. You avoid to write boiler-plate code because you do not have to implement general functionality in each part of the architecture. You just have to implement additional stuff like special queries. For instance you have an News Entity and you want to diplay all news of the day. Because it is implement only once, all methods doing the same have the same names. Class-casts form abstract to concrete classes are prevented by using Generics.

Disadvantage

Everywhere abstract classes are used, the risk of deep class hirarchies occures. This can be avoided by using composition or transfer logic in sperate classes.

Example Implementation

Data Objects

complete code

Abstract Data Objects

In the example news and categories should be created, deleted and diplayed. The news have to be assigned to a category. At first the base data class "AbstractEntity" is written.

package de.starwit.lirejarp.entity;
import java.io.Serializable;
...

@MappedSuperclass 
public abstract class AbstractEntity implements Serializable {
 
 
private static final long serialVersionUID = 1L;
 
private Long id;

 
@Id
  @GeneratedValue
(strategy=GenerationType.IDENTITY)
 
@Column(name="id")
 
public Long getId() {
   
return id;
 
}

 
public void setId(Long id) {
   
this.id = id;
 
}

}

Concrete Data Objects

The entities "CategryEntity" and "NewsEntity" are created and inherit the base-class "AbstractEntity".

package de.starwit.lirejarp.entity;
import java.util.List;
...

@Entity
@Table
(name = "CATEGORY")
public class CategoryEntity extends AbstractEntity {
 
 
private static final long serialVersionUID = -1458424080034108665L;

 
@NotNull
  @Size
(max = 20)
 
private String name;
   
 
private List<NewsEntity> news;

 
@Column(name="NAME", nullable = false, length=100)
 
public String getName() {
   
return name;
 
}

 
public void setName(String name) {
   
this.name = name;
 
}
 
 
@XmlTransient
  @OneToMany
(mappedBy="category")
 
public List<NewsEntity> getNews() {
   
return news;
 
}

 
public void setNews(List<NewsEntity> news) {
   
this.news = news;
 
}
}

package de.starwit.lirejarp.entity;
import java.util.Date;
...

@XmlRootElement
@Entity
@Table
(name="NEWS")
public class NewsEntity extends AbstractEntity {
 
 
private static final long serialVersionUID = -3717230832637971328L;
 
 
@NotBlank
  @Size
(max = 100)
 
private String title;

 
@NotNull
 
private Date publishedAt;
 
 
@NotBlank
  @Size
(max = 100)
 
private String publishedBy;
 
 
@NotBlank
  @Size
(max = 1000)
 
private String content;
 
 
@NotNull
 
private CategoryEntity category;
 
 
@Column(name="TITLE", nullable = false, length=100)
 
public String getTitle() {
   
return title;
 
}

 
public void setTitle(String title) {
   
this.title = title;
 
}
 
 
@Column(name="PUBLISHED_BY", nullable = false, length=100)
 
public String getPublishedBy() {
   
return publishedBy;
 
}

 
public void setPublishedBy(String publishedBy) {
   
this.publishedBy = publishedBy;
 
}

 
@Column(name="CONTENT", nullable = false, length=1000)
 
public String getContent() {
   
return content;
 
}

 
public void setContent(String content) {
   
this.content = content;
 
}

 
@Column(name = "PUBLISHED_AT", nullable = false)
 
@Temporal(TemporalType.TIMESTAMP)
 
public Date getPublishedAt() {
   
return publishedAt;
 
}

 
public void setPublishedAt(Date publishedAt) {
   
this.publishedAt = publishedAt;
 
}
 
 
@ManyToOne
  @JoinColumn
(name = "CATEGORY_ID", nullable = false)
 
public CategoryEntity getCategory() {
   
return category;
 
}

 
public void setCategory(CategoryEntity category) {
   
this.category = category;
 
}
 
 
}

Data Access Services

complete code

Abstract Data Access Services

All starts again with an AbstractService providing CRUD-functionality for all data access services. To ensure correct transaction handling Enterprise Java Beans are taken. For that reason an interface "AbstractService" and the implementation "AbstractServiceImpl" exist.
Abstract Interface
package de.starwit.lirejarp.ejb;
import java.util.List;
...

public interface AbstractService<E extends AbstractEntity> {
 
 
E create(E entity);
 
 
void delete(Long id) throws EntityNotFoundException;
 
  E update
(E entity);
 
  E findById
(Long id);
 
  List<E> findAll
();
 
 
E findByIdWithRelations(Long id, String... attributeName);
 
 
  EntityManager getEntityManager
();

 
void setEntityManager(EntityManager em);

}
Abtract Implementation
package de.starwit.lirejarp.ejb.impl;
import java.lang.reflect.ParameterizedType;
...

public class AbstractServiceImpl<E extends AbstractEntity> implements AbstractService<E> {
 
 
private static Logger LOG = Logger.getLogger(AbstractServiceImpl.class);

 
@PersistenceContext(unitName = "MeineJpaPU")
 
private EntityManager em;

 
private E parentClass;

 
@SuppressWarnings("unchecked")
 
public Class<E> getParentClass() {
   
... 
 
}

 
public EntityManager getEntityManager() {
   
return em;
 
}

 
public E create(E entity) {
   
em.persist(entity);
    em.flush
();
   
return entity;
 
}

 
public void delete(Long id) throws EntityNotFoundException {
   
E entity = em.find(getParentClass(), id);
   
if (entity == null) {
     
throw new EntityNotFoundException(id, getParentClass().getName());
   
}
   
em.remove(entity);
 
}

 
public E update(E entity) {
   
em.merge(entity);
    em.flush
();
   
return entity;
 
}
 
 
public List<E> findAll() {
   
...
 
}
 
 
public List<E> findAll(int firstResult, int maxResult) {
   
...
 
}
 
 
public Long countAll() {
   
...
 
}

 
public E findById(Long id) {
   
return em.find(getParentClass(), id);
 
}

 
public E findByIdWithRelations(Long id, String... relations) {
   
...
 
}
 
 
public E findByIdOrThrowException(Long id)
     
throws IllegalIdException, EntityNotFoundException {
   
...
 
}

 
public void setEntityManager(EntityManager em) {
   
this.em = em;
 
}

}

Concrete Data Access Services

The concrete service interfaces and implementations are using the the concrete entities in the interface-definition and service-implementation-definition. If you do need additional stuff, you can add some functions. Otherwise, it is enough just to create the classes and interfaces without any method. Only the services for the NewsEntity are presented below.
NewsService Interface
package de.starwit.lirejarp.ejb;
import java.io.Serializable;
...

@Local
public interface NewsService extends Serializable, AbstractService<NewsEntity> {

 
List<NewsEntity> findToday();
 
  List<NewsEntity> findByCategory
(Long id);
}
NewsService Implementation
package de.starwit.lirejarp.ejb.impl;
import java.util.Calendar;
...

@Stateless(name="NewsService")
public class NewsServiceImpl extends AbstractServiceImpl<NewsEntity> implements NewsService {
 
 
private static final long serialVersionUID = -1034640519269748512L;

 
@Override
 
public List<NewsEntity> findToday() {
   
Calendar calBefore = Calendar.getInstance();
    calBefore.setTime
(new Date());
    calBefore.set
(Calendar.HOUR_OF_DAY, 0);
    calBefore.set
(Calendar.MINUTE, 0);
    calBefore.set
(Calendar.SECOND, 0);
    calBefore.set
(Calendar.MILLISECOND, 0);

    Calendar calAfter = Calendar.getInstance
();
    calAfter.setTime
(calBefore.getTime());
    calAfter.add
(Calendar.DATE, 1);

    String sql =
"select distinct news from NewsEntity news where news.publishedAt > :dateBefore and news.publishedAt < :dateAfter order by news.publishedAt desc";

    TypedQuery<NewsEntity> query = getEntityManager
().createQuery(
       
sql, NewsEntity.class);
    query.setParameter
("dateBefore", calBefore.getTime());
    query.setParameter
("dateAfter", calAfter.getTime());
   
return query.getResultList();
 
}
 
 
public List<NewsEntity> findByCategory(Long id) {
   
String sql = "select news from NewsEntity news where news.category.id = :id";
    TypedQuery<NewsEntity> query = getEntityManager
().createQuery(
       
sql, NewsEntity.class);
    query.setParameter
("id", id);
   
return query.getResultList();
 
}
 
}

Restful Webservices for User Interface

complete code

In our example, the User Interface is based upon AngularJS and html. Restful webservices are providing the connection between User Interface and the data access layer. The implementation follows the same pattern we used before. With JAXB, some problems occured by using generic types. Hence we switched to Jackson as JSON-Parser. But also Jackson has some limitation handling generic types, additional concrete functions had to be added.

Abstract Restful Webservice

package de.starwit.lirejarp.api.rest;
import java.util.List;
...

public abstract class AbstractRest<E extends AbstractEntity> {
 
 
private static final Logger LOG = Logger.getLogger("fileAppender");

 
/**
   * Deserialisation of JSON is not working. Implement abstract method instead:
   ****************************************************************************  
    @PUT
    @Override
    public ResultStateWrapper create(E entity) {
      return super.createGeneric(entity);
    }
   ****************************************************************************
   *
@param entity
   *
@return
  
*/
 
public abstract ResultWrapper<E> create(E entity);
 
 
/**
   * Deserialisation of JSON is not working. Implement abstract method instead:
   ****************************************************************************  
    @POST
    @Override
    public ResultStateWrapper update(E entity) {
      return super.updateGeneric(entity);
    }
   ****************************************************************************
   *
@param entity
   *
@return
  
*/
 
public abstract ResultStateWrapper update(E entity);
 

 
/**
   * Typify persistence service
   */
 
protected abstract AbstractService<E> getService();
 
 
public ResultWrapper<E> createGeneric(E entity) {
   
LOG.debug("************ FrontendService create for " + getService().getClass().getSimpleName());
    E interalEntity = getService
().create(entity);
    ResultWrapper<E> result =
new ResultWrapper<E>();
    result.setResult
(interalEntity);
    result.setResultState
(ResultValidator.savedResultExists(interalEntity));
   
return result;
 
}
 
 
//Update
 
@POST
 
public ResultStateWrapper updateGeneric(E entity) {
   
E interalEntity = getService().update(entity);
   
return ResultValidator.savedResultExists(interalEntity);
 
}

 
/**
   * returns a flat entity with NO associated entities
   *
   *
@param id
   *
@return
  
*/
 
@Path("/{entityId}")
 
@GET
 
public ResultWrapper<E> getById(@PathParam("entityId") Long id) {
   
E entity = getService().findById(id);
    ResultWrapper<E> rw =
new ResultWrapper<E>(entity);
   
return rw;
 
}
 
 
//Delete
 
@Path("/{entityId}")
 
@DELETE
 
public ResultStateWrapper delete(@PathParam("entityId") Long id) {
   
ResultStateWrapper resultState = new ResultStateWrapper();
   
try {
     
getService().delete(id);
      resultState.setState
(ResultState.OK);
      resultState.setMessage
("Der Eintrag wurde gelöscht.");
   
} catch (EntityNotFoundException e) {
     
resultState.setState(ResultState.NOT_DELETE);
      resultState.setMessage
("Der Eintrag konnte nicht gelöscht werden. " + e.getMessage());
   
}
   
return resultState;
 
}
 
 
 
protected ListResultWrapper<E> genericGetAll() {
   
List<E> entities = getService().findAll();
    ListResultWrapper<E> resultWrapper =
new ListResultWrapper<E>(entities);
    ResultStateWrapper resultStateWrapper = ResultValidator.isNotEmpty
(resultWrapper.getResult());
    resultWrapper.setResultState
(resultStateWrapper);
   
return resultWrapper;
 
}
}

Concrete Restfuls Webservices

package de.starwit.lirejarp.api.rest;
import java.util.Date;
...

@Path("/news")
@Consumes("application/json")
@Produces("application/json")
public class NewsRest extends AbstractRest<NewsEntity> {
 
 
@Inject
 
protected NewsService service;
 
 
@Override
 
protected NewsService getService() {
   
return service;
 
}
 
 
//Create
 
@PUT
  @Override
 
public ResultWrapper<NewsEntity> create(NewsEntity entity) {
   
System.out.println("Category Id: " + entity.getCategory().getId());
    System.out.println
("Category name: " + entity.getCategory().getName());
    entity.setPublishedAt
(new Date());
   
return super.createGeneric(entity);
 
}

 
//Update
 
@POST
  @Override
 
public ResultStateWrapper update(NewsEntity entity) {
   
System.out.println("Category Id: " + entity.getCategory().getId());
    System.out.println
("Category name: " + entity.getCategory().getName());
    entity.setPublishedAt
(new Date());
   
return super.updateGeneric(entity);
 
}
 
 
@Path("/all")
 
@GET
 
public ListResultWrapper<NewsEntity> getAll() {
   
return super.genericGetAll();
 
}
 
 
@Path("/ext/today")
 
@GET
 
public ListResultWrapper<NewsEntity> getToday() {
   
List<NewsEntity> entities = service.findToday();
    ListResultWrapper<NewsEntity> resultWrapper =
new ListResultWrapper<NewsEntity>(entities);
    ResultStateWrapper resultStateWrapper = ResultValidator.isNotEmpty
(resultWrapper.getResult());
    resultWrapper.setResultState
(resultStateWrapper);
   
return resultWrapper; 
 
}
 
 
@Path("/ext/category/{catedoryId}")
 
@GET
 
public ListResultWrapper<NewsEntity> getByCategory(@PathParam("catedoryId") Long id) {
   
List<NewsEntity> entities = service.findByCategory(id);
    ListResultWrapper<NewsEntity> resultWrapper =
new ListResultWrapper<NewsEntity>(entities);
    ResultStateWrapper resultStateWrapper = ResultValidator.isNotEmpty
(resultWrapper.getResult());
    resultWrapper.setResultState
(resultStateWrapper);
   
return resultWrapper; 
 
}
 
}

Keine Kommentare :

Kommentar veröffentlichen