The servlet API, the grizzled workhorse for producing Java websites, is still nimble enough to support RESTful web services as well. There are more recent APIs, among them JAX-RS (Java API for XML-RESTful Services). JAX-RS relies upon Java annotations to advertise the RESTful role that a class and its encapsulated methods play. Jersey is the reference implementation (RI) of JAX-RS. RESTEasy, a JBoss project; Apache Wink; and Apache CXF are other implementations. JAX-RS has APIs for programming RESTful services and clients against such services; the two APIs can be used independently. This section focuses on the service-side API. The first JAX-RS example supports only GET requests, but the second JAX-RS example supports all of the CRUD operations.
JAX-RS web services are resources
that can be published with the Tomcat and Jetty web servers. The first example has
one resource, the class Adages
, and two supporting Java classes: the
deployment class RestfulApplication
and the POJO class Adage
. Exactly how these three classes
interact is covered next.
The RestfulAdage
class (see Example 2-5) extends the
JAX-RS Application
class (line 2), which implements a getClasses
method that enumerates the individual resources
deployed in the WAR file (line 3). In this example, there is but one such resource, Adages
, but there
could be arbitrarily many (line 4).
Example 2-5. The JAX-RS Application
subclass that lists Adages.class as a resource
package
adages
;
import
java.util.Set
;
import
java.util.HashSet
;
import
javax.ws.rs.ApplicationPath
;
import
javax.ws.rs.core.Application
;
@ApplicationPath
(
"/resourcesA"
)
![]()
public
class
RestfulAdage
extends
Application
{
![]()
@Override
public
Set
<
Class
<?>>
getClasses
()
{
![]()
Set
<
Class
<?>>
set
=
new
HashSet
<
Class
<?>>();
set
.
add
(
Adages
.
class
);
![]()
return
set
;
}
}
Recall that any website or web service deployed under Tomcat has a URI that begins with the
name of the deployed WAR file. In the RestfulAdage
class, the annotation ApplicationPath
(line 1)
spells out how the URI continues. For example, assuming that the deployed WAR file is named
adages.war, the ApplicationPath
annotation indicates that the URI part of the URL
continues with resourcesA:
http:
//localhost:8080/adages/resourcesA
The next part is tricky, so the low-level details are explained in a sidebar. At issue is how the
programmer-defined RestfulAdage
class interacts with the Jersey JAX-RS implementation under a Tomcat
deployment. For context,
recall that the getClasses
method (line 3), a callback invoked when the RestfulAdage
instance is loaded into the
servlet container, specifies the JAX-RS resources available in the WAR file. Once again, there
is but a single resource, Adages
(see Example 2-7), in the example.
The RestfulAdage
class is a Jersey Application
because the programmer-defined RestfulAdage
class extends the
JAX-RS Application
class.
If multiple JAX-RS resources were to be
made available in the deployed WAR file, then the class name of each would occur in a
set.add
call in RestfulAdage
. In the current example, there is only:
set
.
add
(
Adages
.
class
);
because Adages
is the only resource.
Publishing JAX-RS Resources with Tomcat explains how a JAX-RS resource can be published with a production-grade web server such as Tomcat; the section also explains how the JAX-RS libraries can be downloaded. For now, the point of interest is that the Jersey implementation of JAX-RS offers other ways to publish, which may be better suited for development. Here is a standalone Java application that publishes the adages service:
package
adages
;
import
java.net.InetSocketAddress
;
import
javax.ws.rs.ext.RuntimeDelegate
;
import
com.sun.net.httpserver.HttpHandler
;
import
com.sun.net.httpserver.HttpServer
;
public
class
AdagesPublisher
{
private
static
final
int
port
=
9876
;
![]()
private
static
final
String
uri
=
"/resourcesA/"
;
![]()
private
static
final
String
url
=
"http://localhost:"
+
port
+
uri
;
public
static
void
main
(
String
[
]
args
)
{
new
AdagesPublisher
().
publish
();
}
private
void
publish
()
{
HttpServer
server
=
getServer
();
HttpHandler
requestHandler
=
RuntimeDelegate
.
getInstance
().
createEndpoint
(
new
RestfulAdage
(),
HttpHandler
.
class
);
server
.
createContext
(
uri
,
requestHandler
);
server
.
start
();
msg
(
server
);
}
private
HttpServer
getServer
()
{
HttpServer
server
=
null
;
int
backlog
=
8
;
try
{
server
=
HttpServer
.
create
(
new
InetSocketAddress
(
"localhost"
,
port
),
backlog
);
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
return
server
;
}
private
void
msg
(
HttpServer
server
)
{
String
out
=
"Publishing RestfulAdage on "
+
url
+
". Hit any key to stop."
;
System
.
out
.
println
(
out
);
try
{
System
.
in
.
read
();
}
catch
(
Exception
e
)
{
}
server
.
stop
(
0
);
// normal termination
}
}
For convenience, this AdagesPublisher
class is in the adages
package together with
Adage
, Adages
, and RestfulAdage
. To compile, the JAR file jersey-core.jar must be on the
classpath; to run, that file and jersey-server.jar must be on the classpath. The ZIP that
contains the sample code has an executable JAR file AdagesPublish.jar that includes all of the
dependencies. The JAR can be executed from the command line:
%
java
-
jar
AdagesPublish
.
jar
The AdagesPublisher
awaits connections on port 9876 (line 1), and the URI (line 2) is
/resourcesA
. Accordingly, the base URL is:
http:
//localhost:9876/resourcesA/
The JAX-RS utility publisher uses classes such as HttpServer
and HttpHandler
, which
come with core Java. Later examples will put these and related classes to use. The point for
now is that there are options for publishing JAX-RS services, including a very lightweight
option. The adages web service performs the same way regardless of how it is published.
The Jersey
implementation does a nice job of cleanly separating JAX-RS services from their publication.
The JAX-RS and Jersey packages do not come with the core Java JDK; instead, the relevant JAR files can be found at jersey.java.net. There is a Maven repository from which a Maven script can install Jersey and its dependencies, but the standalone JAR files are available as well. The Maven approach deliberately hides the deployment details to make life easier for the developer. The goal here, however, is to understand how things work under the hood. In any case, working directly with the JARs is straightforward.
JAX-RS resources can be published as usual with the Ant build.xml script. For example, the command to deploy a JAX-RS resource in the WAR file named adages is:
%
ant
-
Dwar
.
name
=
adages
deploy
As usual, the relevant files would be in a src directory. In this example, the three .java files are in the src/adages subdirectory. The remaining files, including four Jersey JARs, are in src. The relevant JAR files, with approximate sizes, are:
asm
.
jar
;;
43
K
bytes
jersey
-
core
.
jar
;;
206
K
bytes
jersey
-
server
.
jar
;;
595
K
bytes
jersey
-
servlet
.
jar
;;
125
K
bytes
The last three JARs are available, for convenience, in a jersey-bundle.jar.
There are different ways to make these four JARs accessible to Tomcat. The JAR files could be copied to TOMCAT_HOME/lib and thereby be made available to any WAR file deployed under Tomcat. (Recall that Tomcat must be restarted after files are copied to its lib directory in contrast to its webapps directory.) The problem with this approach is version control. Should new versions of the JARs be installed as they come out? If so, will these new versions break already deployed web services? A more conservative approach is to freeze a deployed WAR file by packing the four JARs within the WAR file. This approach also makes it easier to port the WAR from one web server to another, for instance, from Tomcat on one machine to Tomcat on another machine, or from Tomcat to Jetty, and so on. The one downside to packing the JARs inside the WAR is, of course, that the WAR file becomes larger. My preference is to include the required JARs within the WAR file. With this approach, the contents of deployed WAR file adages.war are:
WEB
-
INF
/
web
.
xml
WEB
-
INF
/
classes
/
adages
/
Adage
.
class
WEB
-
INF
/
classes
/
adages
/
Adages
.
class
WEB
-
INF
/
classes
/
adages
/
RestfulAdage
.
class
WEB
-
INF
/
lib
/
asm
.
jar
WEB
-
INF
/
lib
/
jackson
-
annotations
.
jar
WEB
-
INF
/
lib
/
jackson
-
core
.
jar
WEB
-
INF
/
lib
/
jackson
-
databind
.
jar
WEB
-
INF
/
lib
/
jersey
-
core
.
jar
WEB
-
INF
/
lib
/
jersey
-
server
.
jar
WEB
-
INF
/
lib
/
jersey
-
servlet
.
jar
The three JAR files that begin with jackson
handle the generation of JSON documents. Jackson is a collection of Java packages for producing and consuming JSON documents. The main text explains how Jackson works with the rest of the service.
The class adages.RestfulAdage
(see Example 2-5) encapsulates a getClasses
method, whose role can be clarified
with reference to the deployment file web.xml. A JAX-RS service deployed under Tomcat needs
a minimalist web.xml to set up communication between the servlet container and the service.
Here is an example that can be used with any Jersey JAX-RS service published with Tomcat (or Jetty):
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
<
web
-
app
>
<
servlet
>
<
servlet
-
name
>
jersey
</
servlet
-
name
>
<
servlet
-
class
>
com
.
sun
.
jersey
.
spi
.
container
.
servlet
.
ServletContainer
</
servlet
-
class
>
<
load
-
on
-
startup
>
1
</
load
-
on
-
startup
>
</
servlet
>
</
web
-
app
>
The load-on-startup
element prompts Tomcat to instantiate and
load an instance of the Jersey ServletContainer
during the WAR bootstrap process; the critical role of
the ServletContainer
is to scan the deployed WAR file for Jersey Application
classes. Here is a slice of Tomcat’s catalina.out logfile, edited for readability:
INFO:
Deploying
web
application
archive
adages
.
war
INFO:
Registering
Jersey
servlet
application
,
named
adages
.
RestfulAdage
,
![]()
at
the
servlet
mapping
,
/
resources
/*,
with
the
Application
class
of
the
same
name
INFO:
Scanning
for
root
resource
in
the
Web
app
resource
paths:
INFO:
Root
resource
classes
found:
class
adages
.
Adages
![]()
INFO:
Instantiated
the
Application
class
adages
.
RestfulAdage
The upshot of this log segment is that the Jersey ServletContainer
finds the
class RestfulAdage
(line 1), which in turn identifies the JAX-RS
resources in the WAR file (line 2). In this case, there is only one
such resource: Adages
. By the way, if multiple JAX-RS services are
deployed to a servlet container, then each service should have a unique name for the
class that extends Application
. In this first example, the class is named
RestfulAdage
; in a later example, the name is RestfulPrediction
to avoid
conflict.
The JAX-RS service in the deployed WAR file, adages.war, is now ready to accept requests such as:
%
curl
http:
//localhost:8080/adages/resourcesA/
The Adage
class (see Example 2-6) has an import
for the JAX-B
annotation XmlRootElement
. The term binding refers, in this context, to linking a
Java data type such as String
to an XML type, in this case xsd:string
.
Example 2-6. The Adage
POJO class annotated for XML generation through JAX-B
package
adages
;
import
javax.xml.bind.annotation.XmlRootElement
;
@XmlRootElement
(
name
=
"adage"
)
![]()
public
class
Adage
{
private
String
words
;
private
int
wordCount
;
public
Adage
()
{
}
@Override
public
String
toString
()
{
return
words
+
" -- "
+
wordCount
+
" words"
;
}
public
void
setWords
(
String
words
)
{
this
.
words
=
words
;
this
.
wordCount
=
words
.
trim
().
split
(
"\\s+"
).
length
;
}
public
String
getWords
()
{
return
this
.
words
;
}
public
void
setWordCount
(
int
wordCount
)
{
}
public
int
getWordCount
()
{
return
this
.
wordCount
;
}
}
The @XmlRootElement
annotation (line 1) signals that an Adage
object can be transformed into an XML document
whose document or root (that is, outermost) element is named adage
.
For example, the XML document:
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
standalone
=
"yes"
?>
<
adage
>
<
wordCount
>
7
</
wordCount
>
<
words
>
What
can
be
shown
cannot
be
said
.</
words
>
</
adage
>
results from the JAX-B transformation of an in-memory Adage
object.
The Adages
class (see Example 2-7) is a JAX-RS resource that accepts RESTful requests, in
this case only GET requests, and responds with payloads of these three MIME types: text/plain
,
application/json
, and application/xml
.
Example 2-7. The Adages
class as a JAX-RS resource
package
adages
;
import
javax.xml.bind.annotation.XmlElementDecl
;
import
javax.xml.bind.JAXBElement
;
import
javax.xml.namespace.QName
;
import
javax.ws.rs.GET
;
import
javax.ws.rs.Path
;
import
javax.ws.rs.Produces
;
import
javax.ws.rs.core.MediaType
;
import
java.util.Random
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
@Path
(
"/"
)
public
class
Adages
{
// Add aphorisms to taste...
private
String
[
]
aphorisms
=
{
"What can be shown cannot be said."
,
"If a lion could talk, we could not understand him."
,
"Philosophy is a battle against the bewitchment of "
+
"our intelligence by means of language."
,
"Ambition is the death of thought."
,
"The limits of my language mean the limits of my world."
};
public
Adages
()
{
}
@GET
@Produces
({
MediaType
.
APPLICATION_XML
})
// could use "application/xml"
public
JAXBElement
<
Adage
>
getXml
()
{
return
toXml
(
createAdage
());
}
@GET
@Produces
({
MediaType
.
APPLICATION_JSON
})
@Path
(
"/json"
)
public
String
getJson
()
{
return
toJson
(
createAdage
());
}
@GET
@Produces
({
MediaType
.
TEXT_PLAIN
})
@Path
(
"/plain"
)
public
String
getPlain
()
{
return
createAdage
().
toString
()
+
"\n"
;
}
// Create an Adage and set the words property, which
// likewise sets the wordCount property. The adage is
// randomly selected from the array, aphorisms.
private
Adage
createAdage
()
{
Adage
adage
=
new
Adage
();
adage
.
setWords
(
aphorisms
[
new
Random
().
nextInt
(
aphorisms
.
length
)]);
return
adage
;
}
// Java Adage --> XML document
@XmlElementDecl
(
namespace
=
"http://aphorism.adage"
,
name
=
"adage"
)
private
JAXBElement
<
Adage
>
toXml
(
Adage
adage
)
{
return
new
JAXBElement
<
Adage
>(
new
QName
(
"adage"
),
Adage
.
class
,
adage
);
}
// Java Adage --> JSON document
// Jersey provides automatic conversion to JSON using the Jackson
// libraries. In this example, the conversion is done manually
// with the Jackson libraries just to indicate how straightforward it is.
private
String
toJson
(
Adage
adage
)
{
String
json
=
"If you see this, there's a problem."
;
try
{
json
=
new
ObjectMapper
().
writeValueAsString
(
adage
);
}
catch
(
Exception
e
)
{
}
return
json
;
}
}
Perhaps the best way to clarify how the three Java classes interact is through sample client calls. To begin, consider the request:
%
curl
localhost:
8080
/
adages
/
resourcesA
/
plain
On a sample run, the output was:
What
can
be
shown
cannot
be
said
.
--
7
words
The RESTful routing of the client’s request works as follows:
ApplicationPath
, information that the WAR file’s RestfulAdage
provides to the web server.
The next subsegment is /. Recall that the RESTful resource in this web service is the Adages
class, which begins:
@Path
(
"/"
)
public
class
Adages
{
...
The @Path("/")
annotation represents the last slash in the URI adages/resources/. Accordingly, this URI maps to the Adages
class, which is
the one and only JAX-RS resource in the deployed WAR file adages.war.
The final subsegment in the URI is plain, so that the full URI is:
/
adages
/
resources
/
plain
The Adages
method getPlain
is:
@GET
@Produces
({
MediaType
.
TEXT_PLAIN
})
@Path
(
"/plain"
)
public
String
getPlain
()
{
return
createAdage
().
toString
()
+
"\n"
;
}
The @GET
annotation signals that the method/operation getPlain
, an arbitrary name, is accessible through a GET request only.
The @Produces
annotation promises, in effect,
to respond with the MIME type text/plain
. This is a promise rather than a guarantee. The @Path
annotation indicates that the URI subsegment
/plain completes the path to this service operation.
The RESTful routing idioms used in JAX-RS follow the spirit, if not the exact syntax, of those from the Rails framework. These idioms support clear, terse URIs such as:
/
adages
/
resourcesA
/
plain
and:
adages
/
resourcesA
/
json
The interaction between the JAX-RS resource class Adages
and the POJO class Adage
needs clarification. Recall that class Adage
begins:
@XmlRootElement
(
name
=
"adage"
)
public
class
Adage
{
...
and that the annotation @XmlRootElement
allows an Adage
instance to be serialized into an XML document with <adage>
as its document-level
start tag. In the language of JAX-RS, the Adage
class is a provider of XML.
(See the How JAX-B Can Transform a Java Object into an XML Document for details about how JAX-B uses an XML Schema to generate the XML.) Adage
is likewise a POJO class
with the familiar get/set
methods for two properties: words
and wordCount
. The only unusual
detail is that the setWords
method also sets the wordCount
for the adage:
public
void
setWords
(
String
words
)
{
this
.
words
=
words
;
this
.
wordCount
=
words
.
trim
().
split
(
"\\s+"
).
length
;
// word count
}
because this is a convenient way to do so.
The Adages
resource has three methods that define the web service operations: getJson, getPlain,
and getXml. The
operation names are arbitrary. The important routing information for each operation comes from the annotations that
describe the HTTP verb (in this case, only GET) and the @Path
. The getXml operation has no @Path
annotation, which
means that the path for the resource, the Adages
class, is the path for this operation; the path is
/adages/resourcesA/. In effect, getXml is the default operation.
The getJson and getXml operations could be combined into a single operation:
@GET
@Produces
({
MediaType
.
APPLICATION_XML
,
MediaType
.
APPLICATION_JSON
})
...
because Jersey can coordinate directly with the Jackson libraries to process JSON. My implementation uses Jackson explicitly to show just how simple the API is. Further, if the two operations were combined into one, then a client would have to disambiguate the request by adding the HTTP header:
Accept:
application
/
json
to the HTTP request. It seems cleaner to use two different URIs: /adages/resourcesA/ maps to the default getXml operation, whereas /adages/resourcesA/json maps to the getJson operation. Here for review is the utility method that getJson calls to produce the JSON:
private
String
toJson
(
Adage
adage
)
{
String
json
=
"If you see this, there's a problem."
;
try
{
json
=
new
ObjectMapper
().
writeValueAsString
(
adage
);
![]()
}
catch
(
Exception
e
)
{
}
return
json
;
}
The Jackson ObjectMapper
encapsulates the method writeValueAsString
(line 1),
which serializes an Adage
into a JSON document.
The response for a sample request against the toJson operation,
formatted for readability, would look like this:
{
"words"
:
"The limits of my language mean the limits of my world."
,
"wordCount"
:
11
}
Similar serialization occurs with respect to an Adage
converted into an XML document.
The default operation getXml:
@GET
@Produces
({
MediaType
.
APPLICATION_XML
})
// could use "application/xml" instead
public
JAXBElement
<
Adage
>
getXml
()
{
return
toXml
(
createAdage
());
}
returns a JAXBElement<Adage>
, an XML document that represents an Adage
. Under the hood the JAX-B
processor converts an Adage
instance into an XML document. On a sample run the output was:
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
standalone
=
"yes"
?>
<
adage
>
<
wordCount
>
10
</
wordCount
>
<
words
>
If
a
lion
could
talk
,
we
could
not
understand
him
.</
words
>
</
adage
>
The POJO class Adage
currently has but one annotation, @XmlRootElement
. A variety of others could
be used to refine the XML output. Here is a sample refinement:
package
adages
;
...
@XmlRootElement
(
name
=
"adage"
)
@XmlAccessorType
(
XmlAccessType
.
FIELD
)
@XmlType
(
propOrder
=
{
"words"
,
"wordCount"
})
![]()
public
class
Adage
{
@XmlElement
(
required
=
true
)
protected
String
words
;
@XmlElement
(
required
=
true
)
protected
int
wordCount
;
...
The @XmlType
(line 1) is particularly useful if the order of elements in the generated XML document matters.
In the current implementation, the wordCount
element precedes the words
element, but this order
could be reversed through the propOrder
attribute in the @XmlType
annotation (line 1).
This first JAX-RS example illustrates the style of implementing a RESTful web service as a JAX-RS resource. The deployment under Tomcat is uncomplicated, and the adages.war file also can be deployed, as is, under Jetty. The adages service supports only GET requests. The forthcoming adages2 service, implemented as a set of Restlet resources, supports all of the CRUD operations. The next section also shows, with a different example, how JAX-RS nicely supports all of the CRUD operations.
The servlet-based predictions2 service supports the four CRUD operations; hence, the port from the servlet/JSP implementations to
JAX-RS is an opportunity to show the @POST
, @PUT
, and @DELETE
annotations and to illustrate
parametrized versions of the @GET
and @DELETE
operations. This revision highlights again the JAX-RS idioms
for RESTful URIs. The revised service is called predictions3.
The JAX-RS predictions3 service has four Java classes:
RestfulPrediction
class extends the JAX-RS Application
class. When the service WAR file is loaded into Tomcat or Jetty, the Jersey
ServletContainer
finds the RestfulPrediction
instance and invokes its getClasses
method to identify the RESTful
resources in the WAR file. In the case of the RestfulPrediction
service, there is only one resource: the
PredictionsRS
class.
PredictionsRS
class is the RESTful resource, a POJO class whose methods carry annotations such as @GET
and @POST
in
support of the standard CRUD operations. This resource supports MIME-typed requests for XML, JSON, and plain text. A
GET request can ask for all predictions or for a specific one.
Prediction
class is also a POJO class with two properties from before: who
is the author of the prediction and
what
is the prediction itself. There is still an id
property whose value uniquely identifies each Prediction
instance;
prediction instances are stored, in ascending order by id
, in a PredictionsList
, explained in the next bullet point.
The Prediction
class is annotated as an @XmlRootElement
so that Jersey can automatically convert
a single prediction into an XML document. The get-methods of the properties are annotated with
@XmlElement
for emphasis. A Prediction
instance can be transformed into an XML or a JSON document; the Prediction
override
of the toString
method supports a plain-text format as well. The Prediction
class still implements the
Comparable
interface in case sorting is needed. The implementation logic of predictions3 ensures, however, that the
predictions are always sorted by id
in ascending order; hence, additional sorting might put the predictions in descending order
by id
, ascending order by who
, and so on.
PredictionsList
is a class that represents a collection of Prediction
instances, with the collection implemented as
a thread-safe CopyOnWriteArrayList
. The integer id
of each prediction added to the list is generated with a thread-safe
AtomicInteger
.
The class PredictionsList
is annotated as an @XmlRootElement
so that Jersey
automatically serializes a list of predictions, as well as a single prediction, into XML. The PredictionsList
class likewise
overrides the toString
method, yet again a convenient way to support a plain-text representation of a predictions list.
The RestfulPrediction
class (see Example 2-8) is the JAX-RS Application
class. To ensure that the adages
JAX-RS service and this
JAX-RS service can coexist in the same servlet container, the names of the two Application
classes must differ:
in the case of adages, the Application
class is RestfulAdage
; in this case, the Application
class is
RestfulPrediction
.
Example 2-8. Registering the PredictionsRS
class as a JAX-RS resource
package
predictions3
;
import
java.util.Set
;
import
java.util.HashSet
;
import
javax.ws.rs.ApplicationPath
;
import
javax.ws.rs.core.Application
;
@ApplicationPath
(
"/resourcesP"
)
public
class
RestfulPrediction
extends
Application
{
public
Set
<
Class
<?>>
getClasses
()
{
Set
<
Class
<?>>
set
=
new
HashSet
<
Class
<?>>();
set
.
add
(
PredictionsRS
.
class
);
return
set
;
}
}
The backend support for the PredictionsRS
source consists of two POJO classes: Prediction
(see Example 2-9) and
PredictionsList
(see Example 2-10). The class Prediction
is mostly unchanged from the predictions2 version except for
the added @XmlRootElement
annotation, which means that the runtime can automatically convert a
Prediction
instance into an XML document. Details follow shortly.
Example 2-9. The Prediction
class with properties who
, what
, and id
package
predictions3
;
import
javax.xml.bind.annotation.XmlRootElement
;
import
javax.xml.bind.annotation.XmlElement
;
@XmlRootElement
(
name
=
"prediction"
)
public
class
Prediction
implements
Comparable
<
Prediction
>
{
private
String
who
;
// person
private
String
what
;
// his/her prediction
private
int
id
;
// identifier used as lookup key
public
Prediction
()
{
}
@Override
public
String
toString
()
{
return
String
.
format
(
"%2d: "
,
id
)
+
who
+
" ==> "
+
what
+
"\n"
;
}
public
void
setWho
(
String
who
)
{
this
.
who
=
who
;
}
@XmlElement
public
String
getWho
()
{
return
this
.
who
;
}
public
void
setWhat
(
String
what
)
{
this
.
what
=
what
;
}
@XmlElement
public
String
getWhat
()
{
return
this
.
what
;
}
public
void
setId
(
int
id
)
{
this
.
id
=
id
;
}
@XmlElement
public
int
getId
()
{
return
this
.
id
;
}
public
int
compareTo
(
Prediction
other
)
{
return
this
.
id
-
other
.
id
;
}
}
The PredictionsList
POJO class (see Example 2-10) in the predictions3 service is simpler overall than the
Predictions
from class of predictions2 because methods such as populate
have moved into the core JAX-RS class.
In any case, the PredictionsList
class has a find
method to search for a particular Prediction
, and the
data structure used to store the predictions is now a thread-safe CopyOnWriteArrayList
.
Example 2-10. The PredictionsList
class
package
predictions3
;
import
java.util.List
;
import
java.util.concurrent.CopyOnWriteArrayList
;
import
java.util.concurrent.atomic.AtomicInteger
;
import
javax.xml.bind.annotation.XmlElement
;
import
javax.xml.bind.annotation.XmlElementWrapper
;
import
javax.xml.bind.annotation.XmlRootElement
;
@XmlRootElement
(
name
=
"predictionsList"
)
public
class
PredictionsList
{
private
List
<
Prediction
>
preds
;
private
AtomicInteger
predId
;
public
PredictionsList
()
{
preds
=
new
CopyOnWriteArrayList
<
Prediction
>();
predId
=
new
AtomicInteger
();
}
@XmlElement
@XmlElementWrapper
(
name
=
"predictions"
)
public
List
<
Prediction
>
getPredictions
()
{
return
this
.
preds
;
}
public
void
setPredictions
(
List
<
Prediction
>
preds
)
{
this
.
preds
=
preds
;
}
@Override
public
String
toString
()
{
String
s
=
""
;
for
(
Prediction
p
:
preds
)
s
+=
p
.
toString
();
return
s
;
}
public
Prediction
find
(
int
id
)
{
Prediction
pred
=
null
;
// Search the list -- for now, the list is short enough that
// a linear search is ok but binary search would be better if the
// list got to be an order-of-magnitude larger in size.
for
(
Prediction
p
:
preds
)
{
if
(
p
.
getId
()
==
id
)
{
pred
=
p
;
break
;
}
}
return
pred
;
}
public
int
add
(
String
who
,
String
what
)
{
int
id
=
predId
.
incrementAndGet
();
Prediction
p
=
new
Prediction
();
p
.
setWho
(
who
);
p
.
setWhat
(
what
);
p
.
setId
(
id
);
preds
.
add
(
p
);
return
id
;
}
}
The PredictionsRS
class (see Example 2-11) is the JAX-RS resource with annotations that define the CRUD operations. The
class is long enough that inspecting the code in chunks may be helpful.
Example 2-11. The JAX-RS resource PredictionsRS
package
predictions3
;
import
java.io.InputStream
;
import
java.io.BufferedReader
;
import
java.io.InputStreamReader
;
import
javax.ws.rs.GET
;
import
javax.ws.rs.POST
;
import
javax.ws.rs.PUT
;
import
javax.ws.rs.DELETE
;
import
javax.ws.rs.Path
;
import
javax.ws.rs.PathParam
;
import
javax.ws.rs.FormParam
;
import
javax.ws.rs.Produces
;
import
javax.ws.rs.core.MediaType
;
import
javax.ws.rs.core.Context
;
import
javax.ws.rs.core.Response
;
import
javax.ws.rs.core.Context
;
import
javax.servlet.ServletContext
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
@Path
(
"/"
)
public
class
PredictionsRS
{
@Context
private
ServletContext
sctx
;
// dependency injection
private
static
PredictionsList
plist
;
// set in populate()
public
PredictionsRS
()
{
}
@GET
@Path
(
"/xml"
)
@Produces
({
MediaType
.
APPLICATION_XML
})
public
Response
getXml
()
{
checkContext
();
return
Response
.
ok
(
plist
,
"application/xml"
).
build
();
}
@GET
@Path
(
"/xml/{id: \\d+}"
)
@Produces
({
MediaType
.
APPLICATION_XML
})
// could use "application/xml" instead
public
Response
getXml
(
@PathParam
(
"id"
)
int
id
)
{
checkContext
();
return
toRequestedType
(
id
,
"application/xml"
);
}
@GET
@Produces
({
MediaType
.
APPLICATION_JSON
})
@Path
(
"/json"
)
public
Response
getJson
()
{
checkContext
();
return
Response
.
ok
(
toJson
(
plist
),
"application/json"
).
build
();
}
@GET
@Produces
({
MediaType
.
APPLICATION_JSON
})
@Path
(
"/json/{id: \\d+}"
)
public
Response
getJson
(
@PathParam
(
"id"
)
int
id
)
{
checkContext
();
return
toRequestedType
(
id
,
"application/json"
);
}
@GET
@Path
(
"/plain"
)
@Produces
({
MediaType
.
TEXT_PLAIN
})
public
String
getPlain
()
{
checkContext
();
return
plist
.
toString
();
}
@POST
@Produces
({
MediaType
.
TEXT_PLAIN
})
@Path
(
"/create"
)
public
Response
create
(
@FormParam
(
"who"
)
String
who
,
@FormParam
(
"what"
)
String
what
)
{
checkContext
();
String
msg
=
null
;
// Require both properties to create.
if
(
who
==
null
||
what
==
null
)
{
msg
=
"Property 'who' or 'what' is missing.\n"
;
return
Response
.
status
(
Response
.
Status
.
BAD_REQUEST
).
entity
(
msg
).
type
(
MediaType
.
TEXT_PLAIN
).
build
();
}
// Otherwise, create the Prediction and add it to the collection.
int
id
=
addPrediction
(
who
,
what
);
msg
=
"Prediction "
+
id
+
" created: (who = "
+
who
+
" what = "
+
what
+
").\n"
;
return
Response
.
ok
(
msg
,
"text/plain"
).
build
();
}
@PUT
@Produces
({
MediaType
.
TEXT_PLAIN
})
@Path
(
"/update"
)
public
Response
update
(
@FormParam
(
"id"
)
int
id
,
@FormParam
(
"who"
)
String
who
,
@FormParam
(
"what"
)
String
what
)
{
checkContext
();
// Check that sufficient data is present to do an edit.
String
msg
=
null
;
if
(
who
==
null
&&
what
==
null
)
msg
=
"Neither who nor what is given: nothing to edit.\n"
;
Prediction
p
=
plist
.
find
(
id
);
if
(
p
==
null
)
msg
=
"There is no prediction with ID "
+
id
+
"\n"
;
if
(
msg
!=
null
)
return
Response
.
status
(
Response
.
Status
.
BAD_REQUEST
).
entity
(
msg
).
type
(
MediaType
.
TEXT_PLAIN
).
build
();
// Update.
if
(
who
!=
null
)
p
.
setWho
(
who
);
if
(
what
!=
null
)
p
.
setWhat
(
what
);
msg
=
"Prediction "
+
id
+
" has been updated.\n"
;
return
Response
.
ok
(
msg
,
"text/plain"
).
build
();
}
@DELETE
@Produces
({
MediaType
.
TEXT_PLAIN
})
@Path
(
"/delete/{id: \\d+}"
)
public
Response
delete
(
@PathParam
(
"id"
)
int
id
)
{
checkContext
();
String
msg
=
null
;
Prediction
p
=
plist
.
find
(
id
);
if
(
p
==
null
)
{
msg
=
"There is no prediction with ID "
+
id
+
". Cannot delete.\n"
;
return
Response
.
status
(
Response
.
Status
.
BAD_REQUEST
).
entity
(
msg
).
type
(
MediaType
.
TEXT_PLAIN
).
build
();
}
plist
.
getPredictions
().
remove
(
p
);
msg
=
"Prediction "
+
id
+
" deleted.\n"
;
return
Response
.
ok
(
msg
,
"text/plain"
).
build
();
}
private
void
checkContext
()
{
if
(
plist
==
null
)
populate
();
}
private
void
populate
()
{
plist
=
new
PredictionsList
();
String
filename
=
"/WEB-INF/data/predictions.db"
;
InputStream
in
=
sctx
.
getResourceAsStream
(
filename
);
// Read the data into the array of Predictions.
if
(
in
!=
null
)
{
try
{
BufferedReader
reader
=
new
BufferedReader
(
new
InputStreamReader
(
in
));
int
i
=
0
;
String
record
=
null
;
while
((
record
=
reader
.
readLine
())
!=
null
)
{
String
[]
parts
=
record
.
split
(
"!"
);
addPrediction
(
parts
[
0
],
parts
[
1
]);
}
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
"I/O failed!"
);
}
}
}
private
int
addPrediction
(
String
who
,
String
what
)
{
int
id
=
plist
.
add
(
who
,
what
);
return
id
;
}
// Prediction --> JSON document
private
String
toJson
(
Prediction
prediction
)
{
String
json
=
"If you see this, there's a problem."
;
try
{
json
=
new
ObjectMapper
().
writeValueAsString
(
prediction
);
}
catch
(
Exception
e
)
{
}
return
json
;
}
// PredictionsList --> JSON document
private
String
toJson
(
PredictionsList
plist
)
{
String
json
=
"If you see this, there's a problem."
;
try
{
json
=
new
ObjectMapper
().
writeValueAsString
(
plist
);
}
catch
(
Exception
e
)
{
}
return
json
;
}
// Generate an HTTP error response or typed OK response.
private
Response
toRequestedType
(
int
id
,
String
type
)
{
Prediction
pred
=
plist
.
find
(
id
);
if
(
pred
==
null
)
{
String
msg
=
id
+
" is a bad ID.\n"
;
return
Response
.
status
(
Response
.
Status
.
BAD_REQUEST
).
entity
(
msg
).
type
(
MediaType
.
TEXT_PLAIN
).
build
();
}
else
if
(
type
.
contains
(
"json"
))
return
Response
.
ok
(
toJson
(
pred
),
type
).
build
();
else
return
Response
.
ok
(
pred
,
type
).
build
();
// toXml is automatic
}
}
A summary of the major parts of this class follows:
There are five operations annotated with @GET
.
Three of the @GET
operations return the entire list of predictions: in
XML, in JSON, and in plain-text format. In addition, there is a parametrized GET as follows:
@GET
@Path
(
"/xml/{id: \\d+}"
)
@Produces
({
MediaType
.
APPLICATION_XML
})
// could use "application/xml" instead
public
Response
getXml
(
@PathParam
(
"id"
)
int
id
)
{
...
If successful, it returns a single prediction in XML. There is a similarly parametrized GET that,
if successful, returns a
single prediction in JSON. The @Path
annotation:
@Path
(
"/xml/{id: \\d+}"
)
contains a parameter, id
, together with a regular expression that restricts the values of the
parameter to one or more decimal digits. The regular expression also could be written as:
@Path
(
"/xml/{id: [0-9]+}"
)
In either case, the regular expression requires at least one but perhaps more decimal digits. A request such as:
%
curl
http:
//localhost:8080/predictions3/resourcesP/xml/13
would succeed and return, as an XML document, the Prediction
with id
13 because 13
matches the
specified pattern. However, a request such as:
%
curl
http:
//localhost:8080/predictions3/resourcesP/xml/foo
would result in an HTTP 404 (Not Found) error because foo
does not match the one-or-more-decimal-digits pattern.
The JAX-RS support for precise data validation through regular expressions is, of course, convenient; the only
complication comes in the regular expressions themselves.
The methods that implement parametrized GET operations use the @PathParam
annotation to
identify which argument in a method corresponds to the URI parameter. For example, the getJson
method begins:
@GET
@Produces
({
MediaType
.
APPLICATION_JSON
})
@Path
(
"/json/{id: \\d+}"
)
public
Response
getJson
(
@PathParam
(
"id"
)
int
id
)
{
![]()
...
The @PathParam
named id
(line 1) corresponds, in this example, to the int
parameter named
id
as well. The two names could differ. If a URI had multiple parameters:
http:
//...:8080/greetings/resourcesG/msg/fred/Hi
then the order of the Java method’s parameters could differ from the order in the URI:
...
@Path
(
"/msg/{who}/{what}
public Response echoMessage(@PathParam("
what
") String p1,
@PathParam("
who
"
)
String
p2
)
{
...
The operations annotated with @POST
, @PUT
, and @DELETE
implement the remaining CRUD operations:
create, update, and delete, respectively.
In the predictions3 service, most of the RESTfully annotated operations return the JAX-RS type Response
. This
gives the operations a common look and feel that accentuates critical features of an HTTP response—the
HTTP status code together with the content type of the response. For simplicity, the getPlain
method
returns a String
.
Here is the return
statement from the
nonparametrized getXml operation, which returns all of the predictions in XML format:
return
Response
.
ok
(
predictions
,
"application/xml"
).
build
();
Consider the contrast between this return
statement and its counterpart in the adages JAX-RS service:
@GET
@Produces
({
MediaType
.
APPLICATION_XML
})
// could use "application/xml" instead
public
JAXBElement
<
Adage
>
getXml
()
{
return
toXml
(
createAdage
());
}
In the adages case, the return type is JAXBElement
, and there is an explicit call to the toXml
method to convert an Adage
instance into an XML document:
@XmlElementDecl
(
namespace
=
"http://aphorism.adage"
,
name
=
"adage"
)
private
JAXBElement
<
Adage
>
toXml
(
Adage
adage
)
{
return
new
JAXBElement
<
Adage
>(
new
QName
(
"adage"
),
Adage
.
class
,
adage
);
}
By contrast, the predictions3 service simply returns a Predictions
instance as the Response
without
wrapping the predictions
reference in a call to toXml
:
//**** No need to invoke toXml on the predictions!
return
Response
.
ok
(
toXml
(
predictions
),
"application/xml"
).
build
();
The reason for this simplification is that, with Response
as the return type of the getXml
method, the
JAX-RS runtime automatically generates the XML, and JAX-RS runtime does so because the
@Produces
annotation gives application/xml
as the MIME type of the HTTP response. Recall
that the Prediction
and PredictionsList
POJO classes are annotated with @XmlRootElement
. The
combination of this annotation and the @Produces
annotation together automate the XML
generation.
The predictions3 service still has a toJson
utility method to convert one Prediction
or
a collection of these into JSON. This is a design decision, not a necessity. The JAX-RS runtime also generates
JSON automatically if the relevant Jackson libraries are included and if the HTTP request contains
the header element Accept: application/json
. The conversion to JSON is simple enough that
predictions3 does it manually, thereby sparing the client the responsibility of adding a specific header element
to the HTTP request.
The request pattern in the predictions3 service is uniform as there is no default URI—that is,
a URI consisting solely of the slash (/
).
A request for an XML document ends with /xml
for all predictions in XML or, for instance,
/xml/7
to get prediction 7 in XML; a request for JSON ends with /json
or, for example,
/json/13
; and a request for plain text ends with /plain
. The JAX-RS patterns for URIs can adhere
to the Rails URI patterns, now widely imitated, as closely as the programmer likes.
[1] In core Java 8, the functionality of the schemagen utility will give way to general annotation processing through javac.