RESTEasy framework extensions ,GZIP,multi-part form -data

 

RESTEasy framework extensions

RESTEasy is another popular implementation of the JAX-RS specification available under the ASL (Apache) 2.0 license, which can run in any Servlet container. RESTEasy also comes with additional features on top of the plain JAX-RS functionalities. 

To seamlessly integrate the RESTEasy features, the following Maven dependencies must be configured in the project in pom.xml under the dependencies element, as shown ahead:

<!-- RESTEasy Core APIs-->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxb-provider</artifactId>
<version>RELEASE</version>
</dependency>

<!-- RESTEasy Client APIs-->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson-provider</artifactId>
<version>RELEASE</version>
</dependency>

<!-- RESTEasy Cache APIs-->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-cache-core</artifactId>
<version>RELEASE</version>
</dependency>

In the following sections, let's take a look at some of the useful features provided by the RESTEasy framework

Caching using RESTEasy

REST APIs are being used for a majority of B2B interactions, and often, they are constrained by the strict performance obligations to deliver the agreed quality of service. Though network-level caching may be helpful to improve throughput, it does not fully address performance needs. In cases where the underlying state of the data is not prone to frequent changes, such as static data, and there is a significant load on the system, it becomes advantageous to use caching to improve the response time. To facilitate the implementation of caching for RESTful services, RESTEasy comes with the following features:

  • Cache-control annotations for server-side caching
  • Client-side caching
  • Multipart content handling

Cache-control annotations

RESTEasy provides the @Cache and @NoCache annotations, following the JAX-RS specifications, with the @GET annotation for controlling the cache. These annotations can be applied at class and method levels.

The @NoCache annotation is used to specify when caching is not required; hence, the server needs to respond back with a fresh response for every request.

The @Cache annotation is used when the caching of a response is required. Given ahead is a brief list of attributes used to control the caching behavior. Take some time to refer to the previous chapter, which talks in detail about each of these attributes.

  • maxAge: This indicates the maximum time the response message will remain in the cache.
  • sMaxAge: This is the same as maxAge, but it applies for a proxy cache. 
  • noStore: This is used to avoid caching sensitive information set to true.
  • mustRevalidate: If this is true, it revalidates the cache content and serves the fresh response.
  • proxyRevalidate: This is the same as mustRevalidate, but it applies for a proxy cache. 
  • isPrivate: If this is true, the response messages will be cached for a single user only and will not be shared. If false, it means that the response messages can be cached by any cache. 

Given ahead is the application of cache-control annotations for caching the response of the findDepartment function of DepartmentService:

//The response of findDepartment method will be cached for the specified maxAge //duration and user will get the response from cache for same department id //request until the cache expires.

@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
@Cache(maxAge=2000,mustRevalidate = false,noStore = true, proxyRevalidate = false, sMaxAge = 2000)
public Departments findDepartment(@PathParam("id") Short id) {
return entityManager.find(Departments.class, id);
}

In the preceding code snippet, when the client invokes the findDepartment function, the server will respond back with the response from the cache if it exists; otherwise, it will execute the function and return the response. In the latter case, the response will be cached as per the caching definition and will be used to serve future requests. 

To set up the server-side cache, you must register an instance
of org.jboss.resteasy.plugins.cache.server.ServerCacheFeature via your application's getSingletons() or getClasses() method. The underlying cache is Infinispan. By default, RESTEasy will create an Infinispan cache for you.

Client-side caching

With server-side caching, the client will still have to hit the target resource. When the bandwidth is a constraint and there is a requirement to save network-round trips, it makes sense to enable caching at the client side when the consuming system has enough resources required for caching. The HTTP 1.1 protocol specification defines the guidelines around client-side caching with control parameters such as cache-control headers to decide about caching the server response and the cache-expiry mechanisms. 

Following the HTTP specification, RESTEasy provides the BrowserCacheFeature API  for implementing client-side caching, as shown ahead:

//Enabling Client-Side Caching 
public DepartmentServiceClient(){
webTarget = (ResteasyWebTarget) ClientBuilder.newClient().target(BASE_URI);

//Step1: Construct an instance of RESTEasy LightweightBrowserCache
LightweightBrowserCache cache = new LightweightBrowserCache();
//Step2: Default 2 MB of data is the max size, override as relevant
cache.setMaxBytes(20);
//Step3: Apply the BrowserCacheFeature to the target resource to enable caching
BrowserCacheFeature cacheFeature = new BrowserCacheFeature();
cacheFeature.setCache(cache);
webTarget.register(cacheFeature);
}

GZIP compression/decompression

With a lot of mobile applications being used in the market, it becomes essential to enable faster transmission of large chunks of data over a network. RESTEasy provides the GZIP compression/decompression feature. When the response body is plain text, it can be easily compressed on the server side and then decompressed on the client side. RESTEasy accomplishes this with the @GZIP annotation applied at the method level, as shown ahead:

 /**
* Returns list of departments
*
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@GZIP
public List<Departments> findAllDepartments() {
//Find all departments from the data store and return compressed response
javax.persistence.criteria.CriteriaQuery cq = entityManager.getCriteriaBuilder().createQuery();
cq.select(cq.from(Departments.class));
List<Departments> departments = entityManager.createQuery(cq).getResultList();
return departments;
}

The @GZIP annotation of RESTEasy takes care of compressing the response message body and similarly when a server receives a request with content-encoding equal to GZIP then the request message body is decompressed automatically. 

Multipart content handling

RESTEasy provides abstraction of multipart content handling with the multipart data provider plugin. This plugin enables you to easily parse the uploaded content submitted via HTML forms. To use the plugin, include the following Maven dependencies in the POM file:

<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-multipart-provider</artifactId>
<version>RELEASE</version>
</dependency>

Let's try with the simple use case of an employee uploading/downloading his/her profile picture. For this use case, we will create a simple HTML form for the employee to upload his/her profile picture, as shown ahead:

<html>
<body>
<h1>Upload Employee Profile Picture</h1>

<form action="webresources/employee/addimage" method="post" enctype="multipart/form-data">
<p>
Employee Id : <input type="text" name="employeeId" size="50" />
</p>
<p>
Select Profile Image : <input type="file" name="profilePicture" size="50" />
</p>
<p>
Description : <input type="text" name="imageDesc" size="50" />
</p>
<input type="submit" value="Upload Image" />
</form>

</body>
</html>

Please make sure that the form attribute, enctype, is specified as multipart/form-data. Once the user submits the form, the request is submitted to the EmployeeImageResoure class's addImageoperation. The org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput argument of the addImage operation will be used to read the submitted form contents. Similarly, to download the image, the user can use the getProfilePicture operation, which wraps the image file in the response using the javax.ws.rs.core.Response.ResponseBuilder class, as shown ahead:

/**
* This class used to upload or download an employee profile picture
*
*/
@Path("employee")
@Stateless
public class EmployeeImageResource {

@PersistenceContext(unitName = "com.packtpub_rest-chapter5-resteasy-service_war_1.0-SNAPSHOTPU")
private EntityManager entityManager;
private static final Logger logger = Logger.getLogger(EmployeeImageResource.class.getName());

@GET
@Path("/image/{id}")
@Produces("image/png")
/*
* Download the employee profile picture for the given employee id
*/
public Response getProfilePicture(@PathParam("id") Short empId) {

try {

//Step-1: Fetch the employee profile picture from datastore
EmployeeImage profile = entityManager.find(EmployeeImage.class, empId);

//Ste-2:
String fileName = empId + "-image.png";
File imageFile = new File(fileName);
try (FileOutputStream imageFOS = new FileOutputStream(imageFile)) {
imageFOS.write(profile.getEmployeePic());
imageFOS.flush();
}

ResponseBuilder responseBuilder = Response.ok((Object) imageFile);
responseBuilder.header("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
return responseBuilder.build();
} catch (IOException e) {
logger.log(Level.SEVERE, "Error Fetching Employee Profile Picture", e);
return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Error Fetching Employee Profile Picture").build();
}

}


@POST
@Consumes("multipart/form-data")
@Path("/addimage")
/*
* Upload the employee profile picture for the given employee id
*/
public Response addImage(MultipartFormDataInput form) {

try {
//Step-1: Read the Form Contents
Map<String, List<InputPart>> formContents = form.getFormDataMap();
List<InputPart> imagePart = formContents.get("profilePicture");

if (imagePart == null) {
return Response.status(400).entity("Invalid Content Uploaded").build();
}
byte[] profilePic = null;

for (InputPart inputPart : imagePart) {

profilePic = IOUtils.toByteArray(inputPart.getBody(InputStream.class, null));
}

//Step-2: Validate the presence of mandatory content and
//if invalid content reply as Bad Data Request
if (profilePic == null || profilePic.length<1 || formContents.get("employeeId") == null) {
return Response.status(Status.BAD_REQUEST).entity("Invalid Content Uploaded").build();
}

String empId = formContents.get("employeeId").get(0).getBodyAsString();
String desc = "";
if (formContents.get("imageDesc") != null
&& formContents.get("imageDesc").get(0) != null) {
desc = formContents.get("imageDesc").get(0).getBodyAsString();
}

//Step-3:Persist the uploaded image to datastore
EmployeeImage empImgEntity = new EmployeeImage(
Short.parseShort(empId),
profilePic, desc);
entityManager.persist(empImgEntity);

return Response.status(Status.CREATED).entity("Saved Employee Profile Picture").build();

} catch (IOException | NumberFormatException e) {
logger.log(Level.SEVERE, "Error Saving Employee Profile Picture", e);
return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Error Saving Employee Profile Picture").build();

}

}

}































Comments

Popular posts from this blog

Understanding the JAX-RS resource life cycle

Generating a chunked output using Jersey APIs

Jersey client API for reading chunked input