In the end of this lab you should be able to:
Architectural pattern to access information
Fundamental approach: an application is perceived as a collection of resources.
Key implications:
Consider an application that is used to manage contact cards.
A client-server protocol that is stateless.
- each request contains all the information that is necessary to process the request.
Implications:
The REST interface is uniform: all resources are accessed by a set of well-defined HTTP operations:
POST: Creates a new resource
GET: Obtains (a representation of) an existing resource
PUT: Updates or Replaces an existing resource
DELETE: Eliminates an existing resource
Jersey (JAX-RS) is a framework that simplifies the development of REST services in Java.
Java code is instrumented through annotations (e.g., @PATH, @GET, @POST, @DELETE, …)
Java Reflection is used by the Jersey runtime to generate code automatically based on those annotations.
Want to know more? Jersey 3.x
Jersey is split into multiple libraries.
Maven can handle Jersey dependencies via the pom.xml:
<dependencies>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>3.0.4</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-jdk-http</artifactId>
<version>3.0.4</version>
</dependency>
...
</dependencies>
Consider an User management service
A server is responsible for keeping information for all users in the system, it allows:
Resources are modelled as Java classes, with some requirements.
public class User {
private String email;
private String userId;
private String fullName;
private String password;
public User(){
}
public User(String userId, String fullName, String email, String password) {
this.email = email;
this.userId = userId;
this.fullName = fullName;
this.password = password;
}
...
}
Notes:
Java interface enriched with Jersey's annotations.
@Path(RestUsers.PATH)
public interface RestUsers {
public static final String PATH = "/users";
public static final String USER_ID = "userId";
public static final String PASSWORD = "password";
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
String createUser(User user);
@GET
@Path("/{" + USER_ID + "}")
@Produces(MediaType.APPLICATION_JSON)
User getUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password);
...
}
Used to define the base path of the URL used for accessing the service.
@Path(RestUsers.PATH)
public interface RestUsers {
public static final String PATH = "/users";
...
}
The URL of the service is defined by appending the annotation's value to the base URL of the server.
For example, if the server URL is http://myserver:8080/rest, the service will be accessible at http://myserver:8080/rest/users
Used to create a new resource.
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
String createUser(User user);
A HTTP POST request is used when the service is accessed.
Indicates the method will receive an argument through the body of the HTTP request
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
String createUser(User user);
We typically encode Java objects sent in the body of an HTTP request in JavaScript Object Notation (JSON)
Indicates the method will return a value encoded in the body of the HTTP response.
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
String createUser(User user);
Jersey supports various types of encodings, including:
and
byte[]).Used to get the representation of an existing resource.
public static final String USER_ID = "userId";
public static final String PASSWORD = "password";
@GET
@Path("/{" + USER_ID + "}")
@Produces(MediaType.APPLICATION_JSON)
User getUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password);
Used to retrieve optional (query parameter) values from the request.
In a method refers to what follows in addition to the service base path.
public static final String USER_ID = "userId";
@GET
@Path("/{" + USER_ID + "}")
@Produces(MediaType.APPLICATION_JSON)
User getUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password);
Example: http://myserver:8080/rest/users/preguiça?password=12345
The method will be invoked with "preguiça" as the value of userId, and
password will be 12345.
Used in combination to encode and retrieve values as part of request path.
Variables obtained from the path have to be associated with a matching parameter.
Only simple primitive types (and strings) can be passed this way.
Used to update an existing resource.
public static final String USER_ID = "userId";
public static final String PASSWORD = "password";
@PUT
@Path("/{" + USER_ID + "}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
User updateUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password, User user);
The example includes the three ways for passing arguments already shown:
@GET
@Path("/{" + USER_ID + "}")
@Produces(MediaType.APPLICATION_JSON)
User getUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password);
@PUT
@Path("/{" + USER_ID + "}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
User updateUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password, User user);
@DELETE
@Path("/{" + USER_ID + "}")
@Produces(MediaType.APPLICATION_JSON)
User deleteUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password);
Not ambiguous! Endpoints are distinguished by HTTP method (GET, PUT and DELETE, in this example).
Note: @DELETE is used to delete an existing resource...
GET and DELETE are similar.
POST and PUT are similar.
GET should always return a representation of the resource.
A Java class that implements the service API
@Singleton
public class UsersResource implements RestUsers {
...
public UsersResource() {
}
public String createUser(User user) {
...
}
...
}
Used on resources that keep internal state.
Omitted when a stateless server is desired.
Requires a no-args construtor.
public String createUser(User user) {
// Check if user data is valid
if(user.getUserId() == null || user.getPassword() == null || user.getEmail() == null) {
throw new WebApplicationException( Status.BAD_REQUEST );
}
// Check if userId already exists
if( users.containsKey(user.getUserId())) {
throw new WebApplicationException( Status.CONFLICT );
}
users.put(user.getUserId(), user);
return user.getUserId();
}
Annotations can be omitted in the service implementation methods.
WebApplicationException is used to report application errors to the client as HTTP status codes.
if( users.containsKey(user.getUserId())) {
throw new WebApplicationException( Status.CONFLICT );
}
users.put(user.getUserId(), user);
return user.getUserId();
HTTP 200/OK is implicit when the method returns a value normally.
Range 100 – 199: Information (rarely seen)
Range 200 – 299: Success
Range 300 – 399: Redirection: additional action is required
Range 400 – 499: Client Error (e.g., preparing request)
Range 500 – 599: Server Error
REST resources are exposed as endpoints of a HTTP server.
public class UsersServer {
...
public static final int PORT = 8080;
public static final String SERVICE = "UsersService";
private static final String SERVER_URI_FMT = "http://%s:%s/rest";
public static void main(String[] args) {
try {
ResourceConfig config = new ResourceConfig();
config.register(UsersResource.class);
String ip = InetAddress.getLocalHost().getHostAddress();
String serverURI = String.format(SERVER_URI_FMT, ip, PORT);
JdkHttpServerFactory.createHttpServer( URI.create(serverURI), config);
//More code can be executed here...
} catch( Exception e) {
...
}
}
}
ResourceConfig config = new ResourceConfig();
config.register(UsersResource.class);
ResourceConfig is used to register resources in a server.
The server is instanced using its URI.
String ip = InetAddress.getLocalHost().getHostAddress();
String serverURI = String.format(SERVER_URI_FMT, ip, PORT);
JdkHttpServerFactory.createHttpServer( URI.create(serverURI), config);
JdkHttpServerFactory launches a HTTP server in a separate thread.
ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);
WebTarget target = client.target( serverUrl ).path( RestUsers.PATH );
User u = new User( userId, fullName, email, password);
Response r = target.request()
.accept(MediaType.APPLICATION_JSON)
.post(Entity.entity(u, MediaType.APPLICATION_JSON));
if( r.getStatus() == Status.OK.getStatusCode() && r.hasEntity() )
System.out.println("Success, created user with id: " + r.readEntity(String.class) );
else
System.out.println("Error, HTTP error status: " + r.getStatus() );
The example above shows the typical blocks of code for a Jersey client.
ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);
ClientConfig is used for controlling client behavior, such as connection timeouts.
Client represents a Jersey client.
ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);
WebTarget target = client.target( serverUri ).path( RestUsers.PATH );
WebTarget is used to point to a service instance, given its URI;
Can be assembled by concatenation of any number of other elements to the URI;
In the example, the path corresponding to the top-level @Path annotation of the RestUsers interface is appended to the base server URI to yield, something like:
http://myserver:8080/rest/users
Response r = target.request()
.accept(MediaType.APPLICATION_JSON)
.post(Entity.entity(u, MediaType.APPLICATION_JSON));
.request()
.accept(MediaType.APPLICATION_JSON)
.post(Entity.entity(u, MediaType.APPLICATION_JSON));
Notes: Encoding formats must match the formats specified in the service API.
Entity class is used to encode Java objects in the specified format;
Response r = ... ;
if( r.getStatus() == Status.OK.getStatusCode() && r.hasEntity() )
System.out.println("Success, created user with id: " + r.readEntity(String.class) );
else
System.out.println("Error, HTTP error status: " + r.getStatus() );
Response contains both the return status and the value if available.
Note: Response.Status corresponds to the HTTP reply status code and provides a simple explanation of what happened.
With simple class types, use code like:
r.readEntity( String.class );
r.readEntity( String[].class );
r.readEntity( User.class );
With generic class types, use instead:
r.readEntity(new GenericType<List<User>>() {});
r.readEntity(new GenericType<Map<String, User>>() {});
...
ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);
WebTarget target = client.target( serverUrl ).path( RestUsers.PATH );
Response r = target.path( userId )
.queryParam(RestUsers.PASSWORD, password)
.request()
.accept(MediaType.APPLICATION_JSON)
.get();
...
Take notice on:
.path( userId ) concatenates the userId argument to the request path already present in the target;
.queryParam(RestUsers.PASSWORD, password) is used to supply the password argument by appending ?password=<value> request target URI.
Build the image (run in your project folder):
mvn clean compile assembly:single docker:build
Create the docker network sdnet
docker network create -d bridge sdnet
Run the server in a named container (with port forwarding)
docker run -h users-1 --name users-1 --network sdnet -p 8080:8080 sd2122-aula2-xxxxx-yyyyy
Run another container in interactive mode (to execute clients) in a second terminal window
docker run -it --network sdnet sd2122-aula2-xxxxx-yyyyy /bin/bash
Type in the container shell:
java -cp /home/sd/sd2122.jar sd2122.aula2.clients.CreateUserClient http://users-1:8080/rest nmp "Nuno Preguica" nmp@nova.unl.pt 12345
Type in the container shell:
java -cp /home/sd/sd2122.jar sd2122.aula2.clients.GetUserClient http://users-1:8080/rest nmp 12345
GET requests are compatible with normal HTTP requests, like those issued by a browser.
The service is accessible by the host, because:
docker run ... -p 8080:8080 ... exposes port 8080 of the container as port 8080 of the host.
Complete the server;
Complete the clients;
Test your implementations using docker;
Integrate the Discovery class from last week to enable all clients to obtain the server URL automatically.
In the server, implement failure semantics described in the service API, namely:
Moreover, for the searchUsers method, if no pattern is provided you return all users, if there are no users, you return an empty list, and if a pattern is provided you should check each user individually and add it to the return list only if his name contains the exact sequence of chars in the pattern variable (String class has a method that can help you).