Table of Contents
This section introduces the client API and some features such as filters and how to use them with security features in the JDK. The Jersey client API is a high-level Java based API for interoperating with RESTful Web services. It makes it very easy to interoperate with RESTful Web services and enables a developer to concisely and efficiently implement a reusable client-side solution that leverages existing and well established client-side HTTP implementations.
The Jersey client API can be utilized to interoperate with any RESTful Web service, implemented using one of many frameworks, and is not restricted to services implemented using JAX-RS. However, developers familiar with JAX-RS should find the Jersey client API complementary to their services, especially if the client API is utilized by those services themselves, or to test those services.
The goals of the Jersey client API are threefold:
Encapsulate a key constraint of the REST architectural style, namely the Uniform Interface Constraint and associated data elements, as client-side Java artifacts;
Make it as easy to interoperate with RESTful Web services as JAX-RS makes it easy to build RESTful Web services; and
Leverage artifacts of the JAX-RS API for the client side. Note that JAX-RS is currently a server-side only API.
The Jersey Client API supports a pluggable architecture to enable the use of different underlying HTTP client
implementations. Two such implementations are supported and leveraged: the
Http(s)URLConnection
classes
supplied with the JDK; and the Apache HTTP client.
The uniform interface constraint bounds the architecture of RESTful Web services so that a client, such as a browser, can utilize the same interface to communicate with any service. This is a very powerful concept in software engineering that makes Web-based search engines and service mash-ups possible. It induces properties such as:
simplicity, the architecture is easier to understand and maintain; and
modifiability or loose coupling, clients and services can evolve over time perhaps in new and unexpected ways, while retaining backwards compatibility.
Further constraints are required:
every resource is identified by a URI;
a client interacts with the resource via HTTP requests and responses using a fixed set of HTTP methods;
one or more representations can be retured and are identified by media types; and
the contents of which can link to further resources.
The above process repeated over and again should be familiar to anyone who has used a browser to fill in HTML forms and follow links. That same process is applicable to non-browser based clients.
Many existing Java-based client APIs, such as the Apache HTTP client API or
java.net.HttpURLConnection
supplied with the JDK place too much focus on the Client-Server constraint for the exchanges of request and
responses rather than a resource, identified by a URI, and the use of a fixed set of HTTP methods.
A resource in the Jersey client API is an instance of the Java class
WebResource,
and encapsulates a URI. The fixed set of HTTP methods are methods on
WebResource
or if using the builder
pattern (more on this later) are the last methods to be called when invoking an HTTP method on a resource.
The representations are Java types, instances of which, may contain links that new instances of
WebResource
may be created from.
Since a resource is represented as a Java type it makes it easy to configure, pass around and inject in ways that is not so intuitive or possible with other client-side APIs.
The Jersey Client API reuses many aspects of the JAX-RS and the Jersey implementation such as:
URI building using UriBuilder and UriTemplate to safely build URIs;
Support for Java types of representations such as
byte[]
,
String
,
InputStream
,
File
,
DataSource
and JAXB beans in addition to Jersey specific features such as
JSON support and
MIME Multipart support.
Using the builder pattern to make it easier to construct requests.
Some APIs, like the Apache HTTP client or java.net.HttpURLConnection, can be rather hard to use and/or require too much code to do something relatively simple.
This is why the Jersey Client API provides support for wrapping HttpURLConnection and the Apache HTTP client. Thus it is possible to get the benefits of the established implementations and features while getting the ease of use benefit.
It is not intuitive to send a POST request with form parameters and receive a response as a JAXB object with such an API. For example with the Jersey API this is very easy:
Example 3.1. POST request with form parameters
1 Form f = new Form(); 2 f.add("x", "foo"); 3 f.add("y", "bar"); 4 5 Client c = Client.create(); 6 WebResource r = c.resource("http://localhost:8080/form"); 7 8 JAXBBean bean = r. 9 type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 10 .accept(MediaType.APPLICATION_JSON_TYPE) 11 .post(JAXBBean.class, f);
In the above code a
Form is created with two parameters, a new
WebResource instance is created from a
Client then the
Form
instance is POST
ed to
the resource, identified with the form media type, and the response is requested as an instance of a JAXB bean
with an acceptable media type identifying the Java Script Object
Notation (JSON) format. The Jersey client API manages the serialization of the Form
instance to produce the
request and de-serialization of the response to consume as an instance of a JAXB bean.
If the code above was written using
HttpURLConnection
then the developer would have to write code to serialize the
form sent in the POST request and de-serialize the response to the JAXB bean. In addition further code would have to be written
to make it easy to reuse the same resource “http://localhost:8080/form” that is encapsulated in the
WebResource
type.
Refer to the dependencies chapter, and specifically the Core client section, for details on the dependencies when using the Jersey client with Maven and Ant.
Refer to the Java API documentation for details on the Jersey client API packages and classes.
Refer to the Java API Apache HTTP client documentation for details on how to use the Jersey client API with the Apache HTTP client.
To utilize the client API it is first necessary to create an instance of a Client, for example:
Client c = Client.create();
The client instance can then be configured by setting properties on the map returned from
the
getProperties
methods or by calling the specific setter methods,
for example the following configures the client to perform automatic redirection for
appropriate responses:
c.getProperties().put( ClientConfig.PROPERTY_FOLLOW_REDIRECTS, true);
which is equivalent to the following:
c.setFollowRedirects(true);
Alternatively it is possible to create a Client
instance using a
ClientConfig
object for example:
ClientConfig cc = new DefaultClientConfig(); cc.getProperties().put( ClientConfig.PROPERTY_FOLLOW_REDIRECTS, true); Client c = Client.create(cc);
Once a client instance is created and configured it is then possible to obtain a WebResource instance, which will inherit the configuration declared on the client instance. For example, the following creates a reference to a Web resource with the URI “http://localhost:8080/xyz”:
WebResource r = c.resource("http://localhost:8080/xyz");
and redirection will be configured for responses to requests invoked on the Web resource.
Client
instances are expensive resources. It is recommended a configured
instance is reused for the creation of Web resources. The creation of Web resources, the building
of requests and receiving of responses are guaranteed to be thread safe. Thus a
Client
instance and
WebResource
instances may be shared between multiple threads.
In the above cases a
WebResource
instance will utilize
HttpUrlConnection
or
HttpsUrlConnection
,
if the URI scheme of the
WebResource
is “http” or “https” respectively.
Requests to a Web resource are built using the builder pattern (see RequestBuilder) where the terminating method corresponds to an HTTP method (see UniformInterface). For example,
String response = r.accept( MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE). header("X-FOO", "BAR"). get(String.class);
The above sends a GET request with an
Accept
header of
application/json
,
application/xml
and a
non-standard header
X-FOO
of
BAR
.
If the request has a request entity (or representation) then an instance
of a Java type can be declared in the terminating HTTP method, for
PUT
,
POST
and
DELETE
requests.
For example, the following sends a POST request:
String request = "content"; String response = r.accept( MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE). header("X-FOO", "BAR"). post(String.class, request);
where the String "content" will be serialized as the request
entity (see the section "Java instances and types for representations"
section for further details on the supported Java types). The
Content-Type
of the request entity may be declared
using the type
builder method as follows:
String response = r.accept( MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE). header("X-FOO", "BAR"). type(MediaType.TEXT_PLAIN_TYPE). post(String.class, request);
or alternatively the request entity and type may be declared using the entity method as follows:
String response = r.accept( MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE). header("X-FOO", "BAR"). entity(request, MediaType.TEXT_PLAIN_TYPE). post(String.class);
If the response has a entity (or representation) then the
Java type of the instance required is declared in the terminating HTTP method.
In the above examples a response entity is expected and an instance of String
is requested. The response entity will be de-serialized to a String
instance.
If response meta-data is required then the Java type ClientResponse can be declared from which the response status, headers and entity may be obtained. For example, the following gets both the entity tag and response entity from the response:
ClientResponse response = r.get(ClientResponse.class); EntityTag e = response.getEntityTag(); String entity = response.getEntity(String.class);
If the ClientResponse
type is not utilized and the response
status is greater than or equal to 300 then the runtime exception
UniformInterfaceException
is thrown. This exception may be caught and the ClientResponse
obtained as follows:
try { String entity = r.get(String.class); } catch (UniformInterfaceException ue) { ClientResponse response = ue.getResponse(); }
A new
WebResource
can be created from an existing WebResource
by building from the latter's URI.
Thus it is possible to build the request URI before building the request. For example, the
following appends a new path segment and adds some query parameters:
WebResource r = c.resource("http://localhost:8080/xyz"); MultivaluedMap<String, String> params = new MultivaluedMapImpl(); params.add("foo", "x"); params.add("bar", "y"); String response = r.path("abc"). queryParams(params). get(String.class);
that results in a GET request to the URI "http://localhost:8080/xyz/abc?foo=x&bar=y".
All the Java types for representations supported by the Jersey server side for requests and responses are also supported on the client side. This includes the standard Java types as specified by JAX-RS in section 4.2.4 in addition to JSON, Atom and Multipart MIME as supported by Jersey.
To process a response entity (or representation) as a stream of bytes use InputStream as follows:
InputStream in = r.get(InputStream.class); // Read from the stream in.close();
Note that it is important to close the stream after processing so that resources are freed up.
To POST
a file use File
as follows:
File f = ... String response = r.post(String.class, f);
Refer to the JAXB sample to see how JAXB with XML and JSON can be utilized with the client API (more specifically, see the unit tests).
The support for new application-defined representations as Java types requires the implementation of the same provider-based interfaces as for the server side JAX-RS API, namely MessageBodyReader and MessageBodyWriter, respectively, for request and response entities (or inbound and outbound representations). Refer to the entity provider sample for such implementations utilized on the server side.
Classes or implementations of the provider-based interfaces need to be registered with a
ClientConfig
and passed to the Client
for creation. The following
registers a provider class MyReader
which will be instantiated by Jersey:
ClientConfig cc = new DefaultClientConfig(); cc.getClasses().add(MyReader.class); Client c = Client.create(cc);
The following registers an instance or singleton of MyReader:
ClientConfig cc = new DefaultClientConfig(); MyReader reader = ... cc.getSingletons().add(reader); Client c = Client.create(cc);
Filtering requests and responses can provide useful functionality that is hidden from the application layer of building and sending requests, and processing responses. Filters can read/modify the request URI, headers and entity or read/modify the response status, headers and entity.
The Client
and WebResource
classes extend from
Filterable
and that enables the addition of
ClientFilter
instances. A WebResource
will inherit filters from its creator, which
can be a Client
or another WebResource
.
Additional filters can be added to a WebResource
after it has been
created. For requests, filters are applied
in reverse order, starting with the WebResource
filters and then
moving to the inherited filters.
For responses, filters are applied in order, starting with inherited filters
and followed by the filters added to the WebResource
. All filters
are applied in the order in which they were added.
For instance, in the following example the Client
has two filters added,
filter1
and filter2
, in that order, and the
WebResource
has one filter added, filter3
:
ClientFilter filter1 = ... ClientFilter filter2 = ... Client c = Client.create(); c.addFilter(filter1); c.addFilter(filter2); ClientFilter filter3 = ... WebResource r = c.resource(...); r.addFilter(filter3);
After a request has been built the request is filtered by filter3
,
filter2
and filter1
in that order. After the response
has been received the response is filtered by filter1
,
filter2
and filter3
in that order, before the
response is returned.
Filters are implemented using the “russian doll” stack-based pattern where a filter is responsible for calling the next filter in the ordered list of filters (or the next filter in the “chain” of filters). The basic template for a filter is as follows:
class AppClientFilter extends ClientFilter { public ClientResponse handle(ClientRequest cr) { // Modify the request ClientRequest mcr = modifyRequest(cr); // Call the next filter ClientResponse resp = getNext().handle(mcr); // Modify the response return modifyResponse(resp); } }
The filter modifies the request (if required) by creating a new
ClientRequest
or modifying the state of the passed ClientRequest
before calling the
next filter. The call to the next request will return
the response, a ClientResponse
. The filter
modifies the response (if required) by creating a new ClientResponse
or modifying the state of the returned ClientResponse
.
Then the filter returns the modified response.
Filters are re-entrant and may be called by multiple threads performing requests
and processing responses.
The Jersey Client API currently supports two filters:
A GZIP content encoding filter,
GZIPContentEncodingFilter.
If this filter is added then a request entity is compressed with the
Content-Encoding
of gzip
, and a response entity if compressed with a
Content-Encoding
of gzip
is
decompressed. The filter declares an Accept-Encoding
of gzip
.
A logging filter,
LoggingFilter.
If this filter is added then the request and response headers as well as the entities are logged
to a declared output stream if present, or to System.out
if not.
Often this filter will be placed at the end of the ordered list of
filters to log the request before it is sent
and the response after it is received.
The filters above are good examples that show how to modify or read request and response entities. Refer to the source code of the Jersey client for more details.
The Jersey client API was originally developed to aid the testing of the Jersey server-side, primarily to make it easier to write functional tests in conjunction with the JUnit framework for execution and reporting. It is used extensively and there are currently over 1000 tests.
Embedded servers, Grizzly and a special in-memory server, are utilized to deploy the test-based services. Many of the Jersey samples contain tests that utilize the client API to server both for testing and examples of how to use the API. The samples utilize Grizzly or embedded Glassfish to deploy the services.
The following code snippets are presented from the single unit test
HelloWorldWebAppTest
of the
helloworld-webapp
sample.
The setUp
method, called before a test is executed, creates an instance of
the Glassfish server, deploys the application, and a WebResource
instance
that references the base resource:
@Override protected void setUp() throws Exception { super.setUp(); // Start Glassfish glassfish = new GlassFish(BASE_URI.getPort()); // Deploy Glassfish referencing the web.xml ScatteredWar war = new ScatteredWar( BASE_URI.getRawPath(), new File("src/main/webapp"), new File("src/main/webapp/WEB-INF/web.xml"), Collections.singleton( new File("target/classes"). toURI().toURL())); glassfish.deploy(war); Client c = Client.create(); r = c.resource(BASE_URI); }
The tearDown
method, called after a test is executed,
stops the Glassfish server.
@Override protected void tearDown() throws Exception { super.tearDown(); glassfish.stop(); }
The testHelloWorld
method tests that the response
to a GET
request to the Web resource returns “Hello World”:
public void testHelloWorld() throws Exception { String responseMsg = r.path("helloworld"). get(String.class); assertEquals("Hello World", responseMsg); }
Note the use of the path
method on the WebResource
to
build from the base WebResource
.
The support for security, specifically HTTP authentication and/or cookie management
with Http(s)URLConnection
is limited due to constraints in the API.
There are currently no specific features or properties on the Client
class that
can be set to support HTTP authentication. However, since the client API, by default, utilizes
HttpURLConnection
or HttpsURLConnection
,
it is possible to configure system-wide
security settings (which is obviously not sufficient for multiple client configurations).
For HTTP authentication the java.net.Authenticator
can
be extended and statically registered. Refer to the
Http authentication
document for more details.
For cookie management the java.net.CookieHandler
can be extended
and statically registered. Refer to the
Cookie Management
document for more details.
To utilize HTTP with SSL it is necessary to utilize the “https” scheme.
For certificate-based authentication see the class
HTTPSProperties
for how to set javax.net.ssl.HostnameVerifier
and
javax.net.ssl.SSLContext
.
The support for HTTP authentication and cookies is much better with the Apache HTTP
client than with HttpURLConnection
.
See the Java documentation for the package
com.sun.jersey.client.apache,
ApacheHttpClientState and
ApacheHttpClientConfig for more details.